Fast search autocomplete is no longer a luxury—it is a baseline user expectation. In data-heavy systems, hitting a traditional relational database (like PostgreSQL) for fuzzy search query matching will quickly exhaust connection pools and introduce noticeable latency.
To solve this in a corporate banking onboarding workflow, we integrated Typesense, an open-source, in-memory search engine, with a Node.js and GraphQL backend. Here is how we achieved sub-100ms search latency across millions of records.
Why Typesense?
While Elasticsearch is the industry giant, it is resource-intensive and requires complex configuration. Typesense is built in C++, loads its index entirely into RAM, and is designed specifically for instant, typo-tolerant searchout-of-the-box.
1. Schema Definition & Indexing in Node.js
First, we define our collection schema and initialize the Typesense client in our Node.js environment:
const Typesense = require('typesense');
const client = new Typesense.Client({
nodes: [{
host: 'localhost',
port: '8108',
protocol: 'http'
}],
apiKey: 'xyz-secret-key-abc',
connectionTimeoutSeconds: 2
});
// Defining a schema for Corporate Clients
const clientsSchema = {
name: 'clients',
fields: [
{ name: 'companyName', type: 'string' },
{ name: 'registrationId', type: 'string', facet: true },
{ name: 'country', type: 'string', facet: true },
{ name: 'annualRevenue', type: 'int32' }
],
default_sorting_field: 'annualRevenue'
};
async function initializeSearchIndex() {
try {
await client.collections().create(clientsSchema);
console.log('Typesense index initialized successfully.');
} catch (err) {
console.log('Collection already exists or initialization failed.', err);
}
}
2. Syncing Postgres Data with Typesense
To maintain consistency, we used a write-through pattern in our Node.js repository layer. When a client record is updated or created in Postgres, we synchronously index the change in Typesense:
async function createClientRecord(dbConnection, clientData) {
// 1. Write to Postgres
const newClient = await dbConnection.query(
'INSERT INTO clients(company_name, reg_id, country, revenue) VALUES($1, $2, $3, $4) RETURNING *',
[clientData.name, clientData.regId, clientData.country, clientData.revenue]
);
// 2. Index in Typesense
await client.collections('clients').documents().create({
id: newClient.rows[0].id.toString(),
companyName: newClient.rows[0].company_name,
registrationId: newClient.rows[0].reg_id,
country: newClient.rows[0].country,
annualRevenue: newClient.rows[0].revenue
});
return newClient.rows[0];
}
3. Querying Search with Typo Tolerance
In our GraphQL resolver, we query Typesense using its built-in search parameters, specifying query weights and typo tolerance:
const resolvers = {
Query: {
searchClients: async (_, { query, filterCountry }) => {
const searchParameters = {
q: query,
query_by: 'companyName,registrationId',
filter_by: filterCountry ? `country:=${filterCountry}` : '',
num_typos: 2,
sort_by: 'annualRevenue:desc'
};
const searchResults = await client
.collections('clients')
.documents()
.search(searchParameters);
return searchResults.hits.map(hit => ({
id: hit.document.id,
companyName: hit.document.companyName,
country: hit.document.country,
revenue: hit.document.annualRevenue
}));
}
}
};
Performance Results
By introducing Typesense:
- Average search API latency dropped from 850ms (SQL queries matching pattern strings) to 12ms.
- Fuzzy-search typo tolerance immediately improved search matching rates by 42%.
- Relational database CPU load dropped by 30%, freeing up connection pools for transaction records processing.