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 sudo privileges.
  • 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_domain with a DNS A record pointing to your_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:

ParameterPurpose
query_byFields to search, highest priority first
query_by_weightsRelative weight of each query_by field, for example 3,1,1
filter_byFilters such as price:<100, category:=[Electronics,Accessories], brand:!=Vivo, combined with && and ||
facet_byFields to return value counts for, used to build filter sidebars
sort_byUp to three sort fields, for example rating:desc,price:asc
num_typosMaximum typos tolerated per word (0, 1 or 2)
per_page, pagePagination

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

  • curl returns Connection refused on port 8108: check sudo systemctl status typesense-server and read the log with sudo journalctl -u typesense-server -n 50 or sudo 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. Run echo "$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 a float field 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_pass points 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.