Typesense is an open source, typo-tolerant search engine written in C++. It keeps the index in memory, answers queries in milliseconds and exposes everything through a small REST API. In this tutorial you will install Typesense on Ubuntu 24.04 from the official .deb package, create a collection, import documents, run filtered, faceted and geo searches, create a search-only API key, and publish the API over HTTPS with Nginx.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS (x86_64), for example a CubePath VPS.
- A non-root user with
sudoprivileges. - At least 1 GB of RAM. Typesense holds the whole index in memory, so plan for roughly two to three times the size of your raw data.
- For the HTTPS step: a domain name such as
search.your_domainwith a DNS A record pointing toyour_server_ip.
Step 1 - Installing Typesense
Typesense publishes a Debian package that installs the binary, a systemd unit and a configuration file with a randomly generated admin API key. Install curl and jq first; jq makes the JSON responses readable:
sudo apt update
sudo apt install -y curl jq
Download the package. This guide uses version 30.2; check the Typesense downloads page for the current release and adjust the version number:
curl -fLO https://dl.typesense.org/releases/30.2/typesense-server-30.2-amd64.deb
Install it with apt so any dependencies are resolved:
sudo apt install -y ./typesense-server-30.2-amd64.deb
The package enables and starts the typesense-server service. Check that it is running:
sudo systemctl status typesense-server
● typesense-server.service - Typesense Server
Loaded: loaded (/etc/systemd/system/typesense-server.service; enabled; preset: enabled)
Active: active (running) since ...
Query the health endpoint, which does not require an API key:
curl http://localhost:8108/health
{"ok":true}
Step 2 - Securing the configuration
Open the configuration file created by the package:
sudo nano /etc/typesense/typesense-server.ini
It contains the listen address, the data directory and the generated admin key. By default Typesense listens on all interfaces. Change api-address to 127.0.0.1 so the API is only reachable locally; Nginx will publish it over HTTPS in Step 7:
; Typesense Configuration
[server]
api-address = 127.0.0.1
api-port = 8108
data-dir = /var/lib/typesense
api-key = GENERATED_KEY_KEEP_AS_IS
log-dir = /var/log/typesense
Keep the api-key value the package generated. This is the admin key: it can create and delete collections and keys, so it must never reach a browser or a client application.
Restart the service to apply the change:
sudo systemctl restart typesense-server
Confirm that port 8108 is now bound only to the loopback interface:
sudo ss -tlnp | grep 8108
LISTEN 0 1024 127.0.0.1:8108 0.0.0.0:* users:(("typesense-serve",pid=2841,fd=21))
To avoid pasting the key into every command, export it into a shell variable for the rest of this session:
export TYPESENSE_API_KEY=$(sudo awk -F' = ' '/^api-key/ {print $2}' /etc/typesense/typesense-server.ini)
Step 3 - Creating a collection
A collection is a set of documents that share a schema. You declare each field and its type up front; fields marked "facet": true can be used for faceting. Create a products collection:
curl -s -X POST http://localhost:8108/collections \
-H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "products",
"fields": [
{"name": "name", "type": "string"},
{"name": "description", "type": "string"},
{"name": "category", "type": "string", "facet": true},
{"name": "brand", "type": "string", "facet": true},
{"name": "price", "type": "float", "facet": true},
{"name": "rating", "type": "float"},
{"name": "in_stock", "type": "bool", "facet": true},
{"name": "tags", "type": "string[]", "facet": true},
{"name": "location", "type": "geopoint", "optional": true}
],
"default_sorting_field": "rating"
}' | jq '.name, .num_documents'
"products"
0
You do not declare the id field: every document can carry a string id, and Typesense generates one when it is missing. The default_sorting_field must be a numeric, non-optional field; it orders results when relevance scores are equal.
The most common field types are string, int32, int64, float, bool, geopoint and their array variants such as string[].
Step 4 - Importing documents
For more than a handful of documents, use the import endpoint with JSONL (one JSON object per line). Create a sample file:
nano products.jsonl
{"id": "1", "name": "Wireless Mechanical Keyboard", "description": "Compact 75% layout with hot-swappable switches and Bluetooth", "category": "Electronics", "brand": "Keychron", "price": 99.99, "rating": 4.7, "in_stock": true, "tags": ["keyboard", "bluetooth", "mechanical"], "location": [40.4168, -3.7038]}
{"id": "2", "name": "USB-C Hub 7-in-1", "description": "HDMI, SD card reader and 100 W power delivery", "category": "Electronics", "brand": "Anker", "price": 39.99, "rating": 4.5, "in_stock": true, "tags": ["hub", "usb-c"], "location": [41.3874, 2.1686]}
{"id": "3", "name": "Ergonomic Wireless Mouse", "description": "Vertical mouse that reduces wrist strain", "category": "Electronics", "brand": "Logitech", "price": 79.99, "rating": 4.6, "in_stock": false, "tags": ["mouse", "ergonomic", "wireless"], "location": [40.4168, -3.7038]}
{"id": "4", "name": "Adjustable Monitor Stand", "description": "Aluminium stand with cable management", "category": "Accessories", "brand": "Vivo", "price": 29.99, "rating": 4.3, "in_stock": true, "tags": ["stand", "desk"], "location": [52.3676, 4.9041]}
Import the file. action=upsert creates new documents and replaces existing ones with the same id, so you can rerun the import safely:
curl -s -X POST "http://localhost:8108/collections/products/documents/import?action=upsert" \
-H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
-H "Content-Type: text/plain" \
--data-binary @products.jsonl
The response has one line per document:
{"success":true}
{"success":true}
{"success":true}
{"success":true}
A line with "success":false includes an error field explaining which value did not match the schema.
To change only some fields of an existing document, send a PATCH:
curl -s -X PATCH http://localhost:8108/collections/products/documents/1 \
-H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"price": 89.99}' | jq '.price'
89.99
Step 5 - Searching, filtering and faceting
Searches are GET requests. q is the query text and query_by lists the fields to search, in order of importance. The query below contains a typo, which Typesense corrects:
curl -s "http://localhost:8108/collections/products/documents/search?q=keybord&query_by=name,description" \
-H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
| jq '.found, [.hits[].document.name]'
1
[
"Wireless Mechanical Keyboard"
]
For queries with several parameters, let curl URL-encode them with -G and --data-urlencode. This search finds wireless products that are in stock and cost less than 100, sorts them by price and returns facet counts for category and brand:
curl -s -G "http://localhost:8108/collections/products/documents/search" \
-H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
--data-urlencode "q=wireless" \
--data-urlencode "query_by=name,description,tags" \
--data-urlencode "filter_by=in_stock:=true && price:<100" \
--data-urlencode "facet_by=category,brand" \
--data-urlencode "sort_by=price:asc" \
| jq '[.hits[].document.name], .facet_counts[].counts'
[
"Wireless Mechanical Keyboard"
]
[
{
"count": 1,
"highlighted": "Electronics",
"value": "Electronics"
}
]
[
{
"count": 1,
"highlighted": "Keychron",
"value": "Keychron"
}
]
The ergonomic mouse also matches "wireless" but is excluded by in_stock:=true. The most useful search parameters are:
| Parameter | Purpose |
|---|---|
query_by | Fields to search, highest priority first |
query_by_weights | Relative weight of each query_by field, for example 3,1,1 |
filter_by | Filters such as price:<100, category:=[Electronics,Accessories], brand:!=Vivo, combined with && and || |
facet_by | Fields to return value counts for, used to build filter sidebars |
sort_by | Up to three sort fields, for example rating:desc,price:asc |
num_typos | Maximum typos tolerated per word (0, 1 or 2) |
per_page, page | Pagination |
Geo search
Fields of type geopoint store [latitude, longitude]. Use q=* to match all documents, filter by a radius and sort by distance. This finds products within 700 km of Madrid, nearest first:
curl -s -G "http://localhost:8108/collections/products/documents/search" \
-H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
--data-urlencode "q=*" \
--data-urlencode "filter_by=location:(40.4168, -3.7038, 700 km)" \
--data-urlencode "sort_by=location(40.4168, -3.7038):asc" \
| jq '[.hits[] | {name: .document.name, meters: .geo_distance_meters.location}]'
The Amsterdam product is left out because it is farther than 700 km, and each hit reports its distance in geo_distance_meters.
Step 6 - Creating a search-only API key
Client applications and browsers should use a key that can only search. Create one limited to the products collection:
curl -s -X POST http://localhost:8108/keys \
-H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"description": "Frontend search key",
"actions": ["documents:search"],
"collections": ["products"]
}' | jq '{id, value}'
{
"id": 1,
"value": "k8F2..."
}
The full key is shown only in this response, so store it now. Verify that it can search but cannot write:
curl -s -o /dev/null -w "%{http_code}\n" -X DELETE \
http://localhost:8108/collections/products/documents/4 \
-H "X-TYPESENSE-API-KEY: your_search_only_key"
401
For multi-tenant applications, the official client libraries can derive scoped search keys from this key with an embedded filter such as tenant_id:=42, so each user only sees their own documents.
Step 7 - Publishing the API over HTTPS with Nginx
Typesense now listens only on 127.0.0.1. To let applications on other hosts or browsers reach it, put Nginx in front of it with a Let's Encrypt certificate. Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginx
Create a server block for your subdomain, replacing search.your_domain:
sudo nano /etc/nginx/sites-available/typesense
server {
listen 80;
listen [::]:80;
server_name search.your_domain;
location / {
proxy_pass http://127.0.0.1:8108;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 100m;
}
}
client_max_body_size allows large JSONL imports through the proxy. Enable the site, test the configuration and reload Nginx:
sudo ln -s /etc/nginx/sites-available/typesense /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Allow SSH and web traffic through UFW:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Request a certificate. Certbot edits the server block to add TLS and an HTTP to HTTPS redirect:
sudo certbot --nginx -d search.your_domain
From your local machine, test a search over HTTPS with the search-only key:
curl -s "https://search.your_domain/collections/products/documents/search?q=hub&query_by=name" \
-H "X-TYPESENSE-API-KEY: your_search_only_key"
The response should list the USB-C hub.
Troubleshooting
curlreturnsConnection refusedon port 8108: checksudo systemctl status typesense-serverand read the log withsudo journalctl -u typesense-server -n 50orsudo tail -n 50 /var/log/typesense/typesense-server.log.{"message": "Forbidden - a valid x-typesense-api-key header must be sent."}: the header is missing or the key is wrong. Runecho "$TYPESENSE_API_KEY"to check the variable is set in the current shell.- Import lines with
"success":false: the error usually names the field. Common causes are a string sent for afloatfield or a missing non-optional field. - Memory grows over time: Typesense keeps the index in RAM. Check usage with
curl -s http://localhost:8108/metrics.json -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" | jq, drop fields you never search from the schema, or move to a larger plan. - 502 Bad Gateway from Nginx: Typesense is not running or
proxy_passpoints to the wrong port.
Conclusion
You installed Typesense on Ubuntu 24.04, restricted it to localhost, created a schema, imported documents, ran typo-tolerant, filtered, faceted and geo searches, and published the API over HTTPS with a search-only key for clients. As next steps, integrate one of the official client libraries (JavaScript, Python, PHP, Go) into your application, schedule backups with the /operations/snapshot endpoint, and consider a three-node Typesense cluster when you need high availability.
