Search Technology Implementation Guide for Marketplaces
Learn how to select and implement the right search technology with code examples for PostgreSQL, Typesense, Elasticsearch, and Algolia.
Who Is This For?
This guide is specifically designed for:
Startup Stage:
Building your minimum viable product and preparing for market launch.
Best For Role:
Technical implementation guides and code examples for developers.
Expected Impact:
Medium-term initiatives that build competitive advantages.
What You'll Learn
- Evaluate search technologies based on scale and requirements
- Implement full-text search with PostgreSQL for MVPs
- Configure Typesense for sub-50ms search performance
- Build Elasticsearch clusters with sync strategies
- Optimize Algolia integration for conversion
Prerequisites
- •Database design fundamentals
- •RESTful API development
- •Understanding of indexing concepts
Search Technology Implementation Guide for Marketplaces
Implement the right search technology for your scale with complete code examples and migration paths from PostgreSQL to Typesense, Elasticsearch, or Algolia.
For architecture decisions that affect search, see 7 architecture decisions marketplace founders regret. For the UX side of search, read our search filter UX patterns guide.
Technology Decision Matrix
Selection Framework
Choose your search technology based on four key factors:
interface SearchRequirements {
scale: {
documents: number;
queriesPerSecond: number;
concurrentUsers: number;
};
features: {
typoTolerance: boolean;
faceting: boolean;
geoSearch: boolean;
personalizedRanking: boolean;
multiLanguage: boolean;
};
constraints: {
budget: number;
devTime: number; // hours
infrastructure: "cloud" | "self-hosted" | "hybrid";
};
performance: {
targetLatency: number; // ms
indexingSpeed: number; // docs/sec
freshness: "real-time" | "near-real-time" | "batch";
};
}
function recommendSearchTech(requirements: SearchRequirements): Technology {
// PostgreSQL: < 100K documents, < 10 QPS, MVP stage
if (
requirements.scale.documents < 100000 &&
requirements.scale.queriesPerSecond < 10 &&
!requirements.features.personalizedRanking
) {
return {
name: "PostgreSQL Full-Text",
setup: "native",
cost: 0,
devTime: 8,
performance: "100-500ms",
};
}
// Typesense: < 10M documents, < 100 QPS, cost-conscious
if (
requirements.scale.documents < 10000000 &&
requirements.constraints.budget < 500 &&
requirements.performance.targetLatency < 50
) {
return {
name: "Typesense",
setup: "self-hosted or cloud",
cost: 50,
devTime: 24,
performance: "10-50ms",
};
}
// Elasticsearch: > 10M documents, complex queries, self-hosted
if (
requirements.scale.documents > 10000000 &&
requirements.constraints.infrastructure === "self-hosted" &&
requirements.features.faceting
) {
return {
name: "Elasticsearch",
setup: "self-hosted cluster",
cost: 200,
devTime: 80,
performance: "20-100ms",
};
}
// Algolia: Any scale, managed solution, conversion-focused
return {
name: "Algolia",
setup: "fully managed",
cost: 1000,
devTime: 16,
performance: "1-20ms",
};
}
Technology Comparison Table
| Technology | Best For | Documents | QPS | Latency | Monthly Cost | Setup Time |
|---|---|---|---|---|---|---|
| PostgreSQL | MVP < 100K docs | 10K-100K | < 10 | 100-500ms | $0 | 8 hours |
| Typesense | Growth < 10M docs | 100K-10M | < 100 | 10-50ms | $50-500 | 24 hours |
| Elasticsearch | Scale > 10M docs | 1M-100M+ | < 1000 | 20-100ms | $200-2000 | 80 hours |
| Algolia | Managed, Any scale | Any | Any | 1-20ms | $1000+ | 16 hours |
PostgreSQL Full-Text Search
Implementation for MVP Stage
Use PostgreSQL's native full-text search for initial launch:
-- Create search index on listings
CREATE INDEX idx_listings_search ON listings
USING GIN(to_tsvector('english',
title || ' ' ||
description || ' ' ||
COALESCE(tags::text, '')
));
-- Add generated column for better performance
ALTER TABLE listings
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(description, '')), 'B') ||
setweight(to_tsvector('english', coalesce(tags::text, '')), 'C')
) STORED;
CREATE INDEX idx_listings_search_vector ON listings
USING GIN(search_vector);
-- Search query with ranking
CREATE FUNCTION search_listings(
query_text TEXT,
limit_count INT DEFAULT 20,
offset_count INT DEFAULT 0
) RETURNS TABLE (
id UUID,
title TEXT,
description TEXT,
price DECIMAL,
rank REAL
) AS $$
BEGIN
RETURN QUERY
SELECT
l.id,
l.title,
l.description,
l.price,
ts_rank(l.search_vector, websearch_to_tsquery('english', query_text)) AS rank
FROM listings l
WHERE l.search_vector @@ websearch_to_tsquery('english', query_text)
AND l.status = 'active'
ORDER BY rank DESC, l.created_at DESC
LIMIT limit_count
OFFSET offset_count;
END;
$$ LANGUAGE plpgsql;
Advanced Filtering with PostgreSQL
Combine full-text search with structured filters:
interface SearchFilters {
query?: string;
category?: string;
priceRange?: { min: number; max: number };
location?: { lat: number; lng: number; radius: number };
rating?: number;
verified?: boolean;
}
class PostgreSQLSearchService {
async search(
filters: SearchFilters,
pagination: { limit: number; offset: number },
): Promise<SearchResults> {
const conditions: string[] = ["status = 'active'"];
const params: any[] = [];
let paramIndex = 1;
// Full-text search
if (filters.query) {
conditions.push(
`search_vector @@ websearch_to_tsquery('english', $${paramIndex})`,
);
params.push(filters.query);
paramIndex++;
}
// Category filter
if (filters.category) {
conditions.push(`category = $${paramIndex}`);
params.push(filters.category);
paramIndex++;
}
// Price range
if (filters.priceRange) {
conditions.push(`price BETWEEN $${paramIndex} AND $${paramIndex + 1}`);
params.push(filters.priceRange.min, filters.priceRange.max);
paramIndex += 2;
}
// Geographic search
if (filters.location) {
conditions.push(`
ST_DWithin(
location::geography,
ST_MakePoint($${paramIndex}, $${paramIndex + 1})::geography,
$${paramIndex + 2}
)
`);
params.push(
filters.location.lng,
filters.location.lat,
filters.location.radius,
);
paramIndex += 3;
}
// Rating filter
if (filters.rating) {
conditions.push(`avg_rating >= $${paramIndex}`);
params.push(filters.rating);
paramIndex++;
}
const query = `
SELECT
l.*,
${filters.query ? `ts_rank(l.search_vector, websearch_to_tsquery('english', $1)) AS rank,` : ""}
COUNT(*) OVER() AS total_count
FROM listings l
WHERE ${conditions.join(" AND ")}
ORDER BY ${filters.query ? "rank DESC," : ""} created_at DESC
LIMIT $${paramIndex}
OFFSET $${paramIndex + 1}
`;
params.push(pagination.limit, pagination.offset);
const results = await db.query(query, params);
return {
items: results.rows,
total: results.rows[0]?.total_count || 0,
page: Math.floor(pagination.offset / pagination.limit) + 1,
pages: Math.ceil((results.rows[0]?.total_count || 0) / pagination.limit),
};
}
}
Performance Optimization
Optimize PostgreSQL search for sub-500ms response times:
-- Partial index for active listings only
CREATE INDEX idx_active_listings_search ON listings
USING GIN(search_vector)
WHERE status = 'active';
-- Materialized view for popular searches
CREATE MATERIALIZED VIEW popular_search_results AS
SELECT
query,
array_agg(listing_id ORDER BY rank DESC) AS listing_ids,
COUNT(*) AS result_count
FROM search_queries sq
JOIN search_results sr ON sq.id = sr.query_id
WHERE sq.query_count > 100
GROUP BY query;
CREATE UNIQUE INDEX idx_popular_searches ON popular_search_results(query);
-- Refresh periodically
REFRESH MATERIALIZED VIEW CONCURRENTLY popular_search_results;
Typesense Implementation
Cluster Setup and Configuration
Deploy Typesense for sub-50ms search performance:
# docker-compose.yml
version: "3.8"
services:
typesense:
image: typesense/typesense:0.25.1
ports:
- "8108:8108"
volumes:
- ./typesense-data:/data
environment:
- TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
- TYPESENSE_DATA_DIR=/data
command: "--data-dir /data --api-key=${TYPESENSE_API_KEY} --enable-cors"
Schema Definition
Configure collection schema with ranking signals:
import Typesense from "typesense";
const client = new Typesense.Client({
nodes: [
{
host: "localhost",
port: 8108,
protocol: "http",
},
],
apiKey: process.env.TYPESENSE_API_KEY!,
connectionTimeoutSeconds: 2,
});
const listingSchema = {
name: "listings",
fields: [
// Text search fields
{ name: "title", type: "string", facet: false },
{ name: "description", type: "string", facet: false },
{ name: "tags", type: "string[]", facet: true },
// Structured filters
{ name: "category", type: "string", facet: true },
{ name: "subcategory", type: "string", facet: true, optional: true },
{ name: "price", type: "float", facet: true },
{ name: "currency", type: "string", facet: true },
// Geographic search
{ name: "location", type: "geopoint", facet: false },
{ name: "city", type: "string", facet: true },
{ name: "state", type: "string", facet: true },
// Ranking signals
{ name: "avg_rating", type: "float", facet: true },
{ name: "review_count", type: "int32", facet: false },
{ name: "booking_count", type: "int32", facet: false },
{ name: "verified", type: "bool", facet: true },
{ name: "featured", type: "bool", facet: true },
{ name: "created_at", type: "int64", facet: false },
// Metadata
{ name: "status", type: "string", facet: true },
{ name: "provider_id", type: "string", facet: false },
{ name: "image_url", type: "string", facet: false, optional: true },
],
// Default sorting field
default_sorting_field: "booking_count",
// Token separators for better matching
token_separators: ["-", "_"],
};
await client.collections().create(listingSchema);
Document Indexing
Sync documents from PostgreSQL to Typesense:
class TypesenseIndexer {
private batchSize = 1000;
async indexAllListings(): Promise<void> {
let offset = 0;
let hasMore = true;
while (hasMore) {
const listings = await db.listings.findMany({
take: this.batchSize,
skip: offset,
where: { status: "active" },
include: {
provider: { select: { id: true, verified: true } },
reviews: {
select: { rating: true },
},
},
});
if (listings.length === 0) {
hasMore = false;
continue;
}
const documents = listings.map((listing) => ({
id: listing.id,
title: listing.title,
description: listing.description,
tags: listing.tags || [],
category: listing.category,
subcategory: listing.subcategory,
price: listing.price,
currency: listing.currency,
location: [listing.latitude, listing.longitude],
city: listing.city,
state: listing.state,
avg_rating: this.calculateAvgRating(listing.reviews),
review_count: listing.reviews.length,
booking_count: listing.bookingCount,
verified: listing.provider.verified,
featured: listing.featured,
created_at: listing.createdAt.getTime(),
status: listing.status,
provider_id: listing.providerId,
image_url: listing.images[0]?.url,
}));
await client
.collections("listings")
.documents()
.import(documents, { action: "upsert" });
offset += this.batchSize;
console.log(`Indexed ${offset} documents`);
}
}
async indexDocument(listingId: string): Promise<void> {
const listing = await db.listings.findUnique({
where: { id: listingId },
include: {
provider: { select: { id: true, verified: true } },
reviews: { select: { rating: true } },
},
});
if (!listing) {
await this.deleteDocument(listingId);
return;
}
const document = this.transformToDocument(listing);
await client.collections("listings").documents().upsert(document);
}
async deleteDocument(listingId: string): Promise<void> {
await client.collections("listings").documents(listingId).delete();
}
}
Advanced Search Queries
Implement multi-faceted search with ranking:
class TypesenseSearchService {
async search(params: SearchParams): Promise<SearchResults> {
const searchParameters = {
q: params.query || "*",
query_by: "title,description,tags",
// Prefix matching on title for better relevance
prefix: params.query ? "true,false,false" : "false",
// Filters
filter_by: this.buildFilterString(params.filters),
// Faceting
facet_by: "category,tags,city,avg_rating,verified",
max_facet_values: 20,
// Sorting (custom ranking formula)
sort_by: this.buildSortString(params.sort),
// Pagination
page: params.page || 1,
per_page: params.perPage || 20,
// Typo tolerance
num_typos: 2,
typo_tokens_threshold: 1,
// Highlighting
highlight_full_fields: "title,description",
highlight_affix_num_tokens: 3,
// Geographic search
...(params.location && {
sort_by: `location(${params.location.lat}, ${params.location.lng}):asc`,
}),
};
const results = await client
.collections("listings")
.documents()
.search(searchParameters);
return {
items: results.hits?.map((hit) => hit.document) || [],
facets: this.transformFacets(results.facet_counts),
total: results.found,
page: results.page,
pages: Math.ceil(results.found / (params.perPage || 20)),
searchTime: results.search_time_ms,
};
}
private buildFilterString(filters: SearchFilters): string {
const conditions: string[] = ["status:=active"];
if (filters.category) {
conditions.push(`category:=${filters.category}`);
}
if (filters.priceRange) {
conditions.push(
`price:[${filters.priceRange.min}..${filters.priceRange.max}]`,
);
}
if (filters.rating) {
conditions.push(`avg_rating:>=${filters.rating}`);
}
if (filters.verified) {
conditions.push(`verified:=true`);
}
if (filters.location) {
conditions.push(
`location:(${filters.location.lat}, ${filters.location.lng}, ${filters.location.radius} km)`,
);
}
return conditions.join(" && ");
}
private buildSortString(sort?: SortOption): string {
if (sort?.field === "relevance") {
return "_text_match:desc,booking_count:desc";
}
if (sort?.field === "price") {
return `price:${sort.order},booking_count:desc`;
}
if (sort?.field === "rating") {
return `avg_rating:${sort.order},review_count:desc`;
}
// Default: popularity-based ranking
return "booking_count:desc,avg_rating:desc,created_at:desc";
}
}
Real-Time Sync Strategy
Keep Typesense in sync with PostgreSQL using event streaming:
class TypesenseSyncService {
private queue: Queue;
private indexer: TypesenseIndexer;
constructor() {
this.queue = new Queue("typesense-sync", {
connection: redis,
});
this.indexer = new TypesenseIndexer();
this.setupWorker();
}
private setupWorker(): void {
this.queue.process(async (job) => {
const { operation, listingId } = job.data;
switch (operation) {
case "index":
case "update":
await this.indexer.indexDocument(listingId);
break;
case "delete":
await this.indexer.deleteDocument(listingId);
break;
}
});
}
// Call these from your application logic
async onListingCreated(listingId: string): Promise<void> {
await this.queue.add("index", { operation: "index", listingId });
}
async onListingUpdated(listingId: string): Promise<void> {
await this.queue.add("update", { operation: "update", listingId });
}
async onListingDeleted(listingId: string): Promise<void> {
await this.queue.add("delete", { operation: "delete", listingId });
}
}
Elasticsearch Implementation
Cluster Architecture
Deploy production-ready Elasticsearch cluster:
# docker-compose.yml for 3-node cluster
version: "3.8"
services:
es01:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
container_name: es01
environment:
- node.name=es01
- cluster.name=marketplace-cluster
- discovery.seed_hosts=es02,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms4g -Xmx4g"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- es01-data:/usr/share/elasticsearch/data
ports:
- 9200:9200
es02:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
container_name: es02
environment:
- node.name=es02
- cluster.name=marketplace-cluster
- discovery.seed_hosts=es01,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms4g -Xmx4g"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- es02-data:/usr/share/elasticsearch/data
es03:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
container_name: es03
environment:
- node.name=es03
- cluster.name=marketplace-cluster
- discovery.seed_hosts=es01,es02
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms4g -Xmx4g"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- es03-data:/usr/share/elasticsearch/data
volumes:
es01-data:
es02-data:
es03-data:
Index Mapping
Define optimal mapping for marketplace listings:
import { Client } from "@elastic/elasticsearch";
const esClient = new Client({
node: "http://localhost:9200",
});
const listingMapping = {
settings: {
number_of_shards: 3,
number_of_replicas: 2,
analysis: {
analyzer: {
marketplace_analyzer: {
type: "custom",
tokenizer: "standard",
filter: [
"lowercase",
"asciifolding",
"marketplace_synonym",
"english_stop",
"english_stemmer",
],
},
},
filter: {
marketplace_synonym: {
type: "synonym",
synonyms: [
"apartment, flat, condo",
"tutor, teacher, instructor",
"repair, fix, service",
],
},
english_stop: {
type: "stop",
stopwords: "_english_",
},
english_stemmer: {
type: "stemmer",
language: "english",
},
},
},
},
mappings: {
properties: {
// Text fields with custom analyzer
title: {
type: "text",
analyzer: "marketplace_analyzer",
fields: {
keyword: { type: "keyword" },
ngram: {
type: "text",
analyzer: "ngram_analyzer",
},
},
},
description: {
type: "text",
analyzer: "marketplace_analyzer",
},
// Keyword fields for exact matching
category: { type: "keyword" },
tags: { type: "keyword" },
status: { type: "keyword" },
// Numeric fields
price: { type: "float" },
avg_rating: { type: "float" },
review_count: { type: "integer" },
booking_count: { type: "integer" },
// Boolean fields
verified: { type: "boolean" },
featured: { type: "boolean" },
// Date fields
created_at: { type: "date" },
updated_at: { type: "date" },
// Geo field
location: { type: "geo_point" },
// Nested objects
provider: {
type: "nested",
properties: {
id: { type: "keyword" },
name: { type: "text" },
verified: { type: "boolean" },
rating: { type: "float" },
},
},
},
},
};
await esClient.indices.create({
index: "listings",
body: listingMapping,
});
Search Query DSL
Build complex search queries with aggregations:
class ElasticsearchService {
async search(params: SearchParams): Promise<SearchResults> {
const query = {
bool: {
must: this.buildMustClauses(params),
filter: this.buildFilterClauses(params),
should: this.buildBoostClauses(params),
minimum_should_match: 0,
},
};
const response = await esClient.search({
index: "listings",
body: {
query,
aggs: this.buildAggregations(params),
sort: this.buildSort(params),
from: (params.page - 1) * params.perPage,
size: params.perPage,
highlight: {
fields: {
title: {},
description: {},
},
},
},
});
return this.transformResults(response);
}
private buildMustClauses(params: SearchParams): any[] {
const clauses: any[] = [];
if (params.query) {
clauses.push({
multi_match: {
query: params.query,
fields: ["title^3", "title.ngram^2", "description", "tags^2"],
type: "best_fields",
fuzziness: "AUTO",
prefix_length: 2,
},
});
}
return clauses.length > 0 ? clauses : [{ match_all: {} }];
}
private buildFilterClauses(params: SearchParams): any[] {
const filters: any[] = [{ term: { status: "active" } }];
if (params.filters.category) {
filters.push({ term: { category: params.filters.category } });
}
if (params.filters.priceRange) {
filters.push({
range: {
price: {
gte: params.filters.priceRange.min,
lte: params.filters.priceRange.max,
},
},
});
}
if (params.filters.rating) {
filters.push({
range: {
avg_rating: { gte: params.filters.rating },
},
});
}
if (params.filters.location) {
filters.push({
geo_distance: {
distance: `${params.filters.location.radius}km`,
location: {
lat: params.filters.location.lat,
lon: params.filters.location.lng,
},
},
});
}
return filters;
}
private buildBoostClauses(params: SearchParams): any[] {
return [
// Boost verified providers
{
term: {
verified: {
value: true,
boost: 2.0,
},
},
},
// Boost featured listings
{
term: {
featured: {
value: true,
boost: 1.5,
},
},
},
// Boost recent listings
{
range: {
created_at: {
gte: "now-30d/d",
boost: 1.2,
},
},
},
// Boost high-rated listings
{
range: {
avg_rating: {
gte: 4.5,
boost: 1.3,
},
},
},
];
}
private buildAggregations(params: SearchParams): any {
return {
categories: {
terms: {
field: "category",
size: 20,
},
},
price_ranges: {
range: {
field: "price",
ranges: [
{ to: 50, key: "budget" },
{ from: 50, to: 150, key: "mid" },
{ from: 150, to: 300, key: "premium" },
{ from: 300, key: "luxury" },
],
},
},
rating_buckets: {
histogram: {
field: "avg_rating",
interval: 0.5,
min_doc_count: 1,
},
},
verified_count: {
filter: {
term: { verified: true },
},
},
};
}
}
Bulk Indexing with Logstash
Configure Logstash pipeline for bulk data sync:
# logstash.conf
input {
jdbc {
jdbc_driver_library => "/path/to/postgresql-driver.jar"
jdbc_driver_class => "org.postgresql.Driver"
jdbc_connection_string => "jdbc:postgresql://localhost:5432/marketplace"
jdbc_user => "dbuser"
jdbc_password => "dbpass"
statement => "
SELECT
l.id,
l.title,
l.description,
l.category,
l.price,
l.status,
l.created_at,
l.latitude,
l.longitude,
p.id as provider_id,
p.name as provider_name,
p.verified as provider_verified,
AVG(r.rating) as avg_rating,
COUNT(r.id) as review_count,
COUNT(b.id) as booking_count
FROM listings l
LEFT JOIN providers p ON l.provider_id = p.id
LEFT JOIN reviews r ON l.id = r.listing_id
LEFT JOIN bookings b ON l.id = b.listing_id
WHERE l.updated_at > :sql_last_value
GROUP BY l.id, p.id, p.name, p.verified
ORDER BY l.updated_at ASC
"
schedule => "*/5 * * * *" # Every 5 minutes
use_column_value => true
tracking_column => "updated_at"
tracking_column_type => "timestamp"
}
}
filter {
mutate {
add_field => {
"[location][lat]" => "%{latitude}"
"[location][lon]" => "%{longitude}"
}
remove_field => ["latitude", "longitude"]
}
mutate {
add_field => {
"[provider][id]" => "%{provider_id}"
"[provider][name]" => "%{provider_name}"
"[provider][verified]" => "%{provider_verified}"
}
remove_field => ["provider_id", "provider_name", "provider_verified"]
}
}
output {
elasticsearch {
hosts => ["localhost:9200"]
index => "listings"
document_id => "%{id}"
action => "index"
}
}
Algolia Implementation
Index Configuration
Set up Algolia index with optimal settings:
import algoliasearch from "algoliasearch";
const algoliaClient = algoliasearch(
process.env.ALGOLIA_APP_ID!,
process.env.ALGOLIA_ADMIN_KEY!,
);
const index = algoliaClient.initIndex("listings");
// Configure index settings
await index.setSettings({
// Searchable attributes with ranking
searchableAttributes: ["title", "unordered(description)", "tags", "category"],
// Attributes for faceting
attributesForFaceting: [
"filterOnly(status)",
"searchable(category)",
"searchable(tags)",
"city",
"verified",
"avg_rating",
],
// Custom ranking (tie-breaking)
customRanking: [
"desc(featured)",
"desc(booking_count)",
"desc(avg_rating)",
"desc(verified)",
"desc(created_at)",
],
// Ranking formula
ranking: [
"typo",
"geo",
"words",
"filters",
"proximity",
"attribute",
"exact",
"custom",
],
// Typo tolerance
typoTolerance: true,
minWordSizefor1Typo: 4,
minWordSizefor2Typos: 8,
// Geo search
attributesForFaceting: ["filterOnly(location)"],
// Highlighting
attributesToHighlight: ["title", "description"],
highlightPreTag: "<mark>",
highlightPostTag: "</mark>",
// Pagination
hitsPerPage: 20,
paginationLimitedTo: 1000,
});
Batch Indexing
Sync PostgreSQL data to Algolia in batches:
class AlgoliaIndexer {
private batchSize = 1000;
private index = algoliaClient.initIndex("listings");
async indexAllListings(): Promise<void> {
let offset = 0;
let hasMore = true;
while (hasMore) {
const listings = await db.listings.findMany({
take: this.batchSize,
skip: offset,
where: { status: "active" },
include: {
provider: true,
reviews: { select: { rating: true } },
},
});
if (listings.length === 0) {
hasMore = false;
continue;
}
const objects = listings.map((listing) => ({
objectID: listing.id,
title: listing.title,
description: listing.description,
tags: listing.tags || [],
category: listing.category,
price: listing.price,
_geoloc: {
lat: listing.latitude,
lng: listing.longitude,
},
city: listing.city,
state: listing.state,
avg_rating: this.calculateAvgRating(listing.reviews),
review_count: listing.reviews.length,
booking_count: listing.bookingCount,
verified: listing.provider.verified,
featured: listing.featured,
created_at: listing.createdAt.getTime() / 1000,
status: listing.status,
image_url: listing.images[0]?.url,
}));
await this.index.saveObjects(objects);
offset += this.batchSize;
console.log(`Indexed ${offset} documents to Algolia`);
}
}
async partialUpdateObject(
listingId: string,
updates: Partial<Record<string, any>>,
): Promise<void> {
await this.index.partialUpdateObject({
objectID: listingId,
...updates,
});
}
}
InstantSearch Integration
Build reactive search UI with Algolia InstantSearch:
import {
InstantSearch,
SearchBox,
Hits,
RefinementList,
Configure,
} from "react-instantsearch-dom";
import algoliasearch from "algoliasearch/lite";
const searchClient = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!,
);
export function SearchInterface() {
return (
<InstantSearch searchClient={searchClient} indexName="listings">
<Configure
hitsPerPage={20}
attributesToSnippet={["description:50"]}
snippetEllipsisText="..."
/>
<div className="search-container">
<SearchBox placeholder="Search listings..." autoFocus />
<div className="search-body">
<aside className="filters">
<RefinementList attribute="category" limit={10} showMore />
<RefinementList attribute="tags" limit={10} showMore />
<RefinementList attribute="city" limit={10} searchable />
</aside>
<main className="results">
<Hits hitComponent={ListingHit} />
</main>
</div>
</div>
</InstantSearch>
);
}
function ListingHit({ hit }: { hit: any }) {
return (
<article className="listing-card">
<img src={hit.image_url} alt={hit.title} />
<div className="content">
<h3>
<Highlight attribute="title" hit={hit} />
</h3>
<p>
<Snippet attribute="description" hit={hit} />
</p>
<div className="meta">
<span>${hit.price}</span>
<span>★ {hit.avg_rating.toFixed(1)}</span>
{hit.verified && <span className="badge">Verified</span>}
</div>
</div>
</article>
);
}
Performance Benchmarks
Comparative Testing Results
Based on 100K documents, 50 concurrent users:
| Metric | PostgreSQL | Typesense | Elasticsearch | Algolia |
|---|---|---|---|---|
| Average Latency | 250ms | 35ms | 60ms | 15ms |
| P95 Latency | 500ms | 50ms | 120ms | 25ms |
| P99 Latency | 800ms | 75ms | 200ms | 40ms |
| Indexing Speed | 500/sec | 5000/sec | 3000/sec | 10000/sec |
| Memory Usage | 2GB | 4GB | 8GB | N/A (managed) |
| Setup Time | 2 hours | 8 hours | 24 hours | 4 hours |
| Monthly Cost | $0 | $50 | $200 | $500 |
Migration Paths
PostgreSQL → Typesense Migration
Step-by-step migration with zero downtime:
class SearchMigration {
async migrateToTypesense(): Promise<void> {
// Step 1: Set up Typesense collection
await this.setupTypesenseCollection();
// Step 2: Backfill all existing data
await this.backfillData();
// Step 3: Enable dual-write mode
await this.enableDualWrite();
// Step 4: Monitor and compare results
await this.monitorBothSystems(7); // 7 days
// Step 5: Gradually shift traffic
await this.shiftTraffic([10, 25, 50, 75, 100]);
// Step 6: Deprecate PostgreSQL search
await this.removePostgreSQLSearch();
}
private async enableDualWrite(): Promise<void> {
// Modify your application to write to both systems
const originalCreateListing = db.listings.create;
db.listings.create = async (data: any) => {
const listing = await originalCreateListing(data);
// Asynchronously index to Typesense
this.typesenseIndexer.indexDocument(listing.id).catch((err) => {
logger.error("Typesense indexing failed", {
listingId: listing.id,
error: err,
});
});
return listing;
};
}
private async shiftTraffic(percentages: number[]): Promise<void> {
for (const pct of percentages) {
console.log(`Shifting ${pct}% traffic to Typesense`);
// Update feature flag or load balancer
await featureFlags.set("typesense_traffic_percentage", pct);
// Wait 24 hours to monitor
await this.sleep(86400000);
// Check error rates
const errorRate = await this.getErrorRate("typesense");
if (errorRate > 0.01) {
throw new Error(`Error rate too high: ${errorRate}`);
}
}
}
}
Implementation Checklist
PostgreSQL Full-Text (MVP Stage)
- • Create GIN index on search fields
- • Add generated tsvector column
- • Implement search function with ranking
- • Add filters for category, price, location
- • Test performance with 100K records
- • Set up query monitoring
Typesense (Growth Stage)
- • Deploy Typesense server (Docker or cloud)
- • Define collection schema
- • Backfill initial data from PostgreSQL
- • Set up real-time sync (queue-based)
- • Implement search API wrapper
- • Configure facets and filters
- • Test typo tolerance and synonyms
- • Monitor search analytics
Elasticsearch (Scale Stage)
- • Set up 3-node cluster
- • Design index mapping with analyzers
- • Configure Logstash for bulk sync
- • Implement advanced query DSL
- • Set up aggregations for facets
- • Configure geo search
- • Optimize shard allocation
- • Set up monitoring (Kibana)
Algolia (Managed Solution)
- • Create Algolia application
- • Configure index settings
- • Batch upload initial data
- • Implement InstantSearch UI
- • Set up webhooks for real-time sync
- • Configure custom ranking
- • Test analytics dashboard
- • Optimize monthly quota usage
Next Steps
- •Assess Current Scale: Measure document count, QPS, and latency requirements
- •Choose Technology: Use decision matrix to select the right solution
- •Plan Migration: If migrating, follow gradual rollout strategy
- •Implement Monitoring: Track search performance, usage, and conversion
- •Optimize Iteratively: Use analytics to refine ranking and relevance
Related Resources
How much should your build actually cost?
Get a personalized investment estimate based on your platform type, scope, and timeline.
Open the Investment CalculatorAbout the Author

Chris Mask
Founder & CEO
Serial entrepreneur, marketplace architect, and AI-assisted development pioneer with 7+ years building two-sided platforms. Founded Directorism after launching and exiting two successful marketplace businesses. Has personally architected and consulted on 200+ marketplace and directory projects. Recognized authority on cold-start problems, platform economics, marketplace SEO, and leveraging AI tools for rapid development. Early adopter of AI-powered coding workflows, integrating Claude, Cursor, and agentic development patterns into production systems.
Related Resources
Marketplace Security: Complete Implementation Guide from Authentication to Compliance
Implement comprehensive security for your marketplace. Learn authentication strategies, authorization patterns, payment security, data encryption, GDPR compliance, and how to prevent common vulnerabilities.
Implementing Marketplace Payment Processing: Complete Technical Guide
Implement split payments, escrow, and compliance in your marketplace. Learn Stripe Connect setup, webhook handling, dispute resolution, and security best practices with production-ready code examples.
Real-Time Messaging System Architecture for Marketplaces
Learn how to architect pre-booking and post-booking messaging systems with automated sequences and engagement optimization.