Search Rules
Defines a complete Elasticsearch search architecture with client config, index classes, query patterns, and sync logic for a TypeScript project.
What this file does
Defines a complete Elasticsearch search architecture with client config, index classes, query patterns, and sync logic for a TypeScript project.
When to use it
- Adding full-text search to a Node.js backend
- Structuring search indexes per entity in Elasticsearch
- Synchronizing database changes with a search index
- Standardizing search query patterns and error handling
Assumes this stack
trigger: always_on
Search Rules
Search Service Architecture
MUST use Elasticsearch for full-text search functionality.
Search Service Structure:
// src/services/elasticsearch/search.ts
import elastic from "./elastic";
import CustomerSearchIndex from "./indexes/CustomerSearchIndex";
class search {
static customers() {
return new CustomerSearchIndex(elastic);
}
}
export default search;
Elasticsearch Client Configuration
MUST configure Elasticsearch client in dedicated file:
// src/services/elasticsearch/elastic.ts
import { Client } from "@elastic/elasticsearch";
const elastic = new Client({
node: process.env.ELASTICSEARCH_URL,
auth: {
username: process.env.ELASTICSEARCH_USERNAME,
password: process.env.ELASTICSEARCH_PASSWORD,
},
});
export default elastic;
Search Index Pattern
MUST create dedicated search index classes for each searchable entity:
// src/services/elasticsearch/indexes/CustomerSearchIndex.ts
import { Client } from "@elastic/elasticsearch";
import Customer from "@/domain/entities/Customer";
class CustomerSearchIndex {
private client: Client;
private indexName = "customers";
constructor(client: Client) {
this.client = client;
}
async index(customer: Customer) {
await this.client.index({
index: this.indexName,
id: customer.id,
document: {
id: customer.id,
firstName: customer.firstName,
lastName: customer.lastName,
email: customer.email,
legalStatuts: customer.legalStatuts,
},
});
}
async search(query: string) {
const response = await this.client.search({
index: this.indexName,
query: {
multi_match: {
query,
fields: ["firstName", "lastName", "email"],
},
},
});
return response.hits.hits.map((hit) => hit._source);
}
}
export default CustomerSearchIndex;
Search Index Naming
MUST use lowercase, singular names for index names:
"customers"(not"Customers"or"customer")"products"(not"Products"or"product")"orders"(not"Orders"or"order")
Domain Search Interface
MUST define search interfaces in domain layer:
// src/domain/search/SearchIndex.ts
interface SearchIndex<T> {
index(entity: T): Promise<void>;
search(query: string): Promise<T[]>;
delete(id: string): Promise<void>;
update(entity: T): Promise<void>;
}
export default SearchIndex;
MUST implement domain interfaces in service layer:
// src/services/elasticsearch/indexes/CustomerSearchIndex.ts
import SearchIndex from "@/domain/search/SearchIndex";
import Customer from "@/domain/entities/Customer";
class CustomerSearchIndex implements SearchIndex<Customer> {
// Implementation here
}
Search Query Patterns
MUST use appropriate Elasticsearch query types:
Multi-field search:
async search(query: string) {
const response = await this.client.search({
index: this.indexName,
query: {
multi_match: {
query,
fields: ["firstName", "lastName", "email"],
type: "best_fields",
},
},
});
return response.hits.hits.map(hit => hit._source);
}
Filtered search:
async searchByStatus(query: string, status: string) {
const response = await this.client.search({
index: this.indexName,
query: {
bool: {
must: [
{
multi_match: {
query,
fields: ["firstName", "lastName", "email"],
},
},
],
filter: [
{
term: {
status,
},
},
],
},
},
});
return response.hits.hits.map(hit => hit._source);
}
Index Management
MUST provide index management methods:
class CustomerSearchIndex {
async createIndex() {
const exists = await this.client.indices.exists({
index: this.indexName,
});
if (!exists) {
await this.client.indices.create({
index: this.indexName,
settings: {
number_of_shards: 1,
number_of_replicas: 0,
},
mappings: {
properties: {
firstName: { type: "text" },
lastName: { type: "text" },
email: { type: "keyword" },
legalStatuts: { type: "keyword" },
},
},
});
}
}
async deleteIndex() {
await this.client.indices.delete({
index: this.indexName,
});
}
}
Document Operations
MUST provide CRUD operations for search documents:
class CustomerSearchIndex {
async index(customer: Customer) {
await this.client.index({
index: this.indexName,
id: customer.id,
document: customer,
});
}
async update(customer: Customer) {
await this.client.update({
index: this.indexName,
id: customer.id,
doc: customer,
});
}
async delete(id: string) {
await this.client.delete({
index: this.indexName,
id,
});
}
async get(id: string) {
const response = await this.client.get({
index: this.indexName,
id,
});
return response._source;
}
}
Error Handling
MUST handle Elasticsearch errors appropriately:
async search(query: string) {
try {
const response = await this.client.search({
index: this.indexName,
query: {
multi_match: {
query,
fields: ["firstName", "lastName", "email"],
},
},
});
return response.hits.hits.map(hit => hit._source);
} catch (error) {
if (error.meta?.statusCode === 404) {
// Index doesn't exist
return [];
}
throw error;
}
}
Search Synchronization
MUST synchronize database changes with search index:
// In service layer after database operations
class CustomerService {
async createCustomer(data: Partial<Customer>): Promise<Customer> {
const customer = await customerRepository.create(data);
// Sync with search index
await search.customers().index(customer);
return customer;
}
async updateCustomer(id: string, data: Partial<Customer>): Promise<Customer> {
const customer = await customerRepository.update(id, data);
// Sync with search index
await search.customers().update(customer);
return customer;
}
async deleteCustomer(id: string): Promise<void> {
await customerRepository.delete(id);
// Remove from search index
await search.customers().delete(id);
}
}
Environment Variables
MUST use environment variables for Elasticsearch configuration:
ELASTICSEARCH_URL=https://your-elasticsearch-cluster.com
ELASTICSEARCH_USERNAME=your-username
ELASTICSEARCH_PASSWORD=your-password
Search Response Format
MUST return consistent response format:
interface SearchResult<T> {
results: T[];
total: number;
page?: number;
pageSize?: number;
}
async search(query: string, page = 1, size = 10): Promise<SearchResult<Customer>> {
const response = await this.client.search({
index: this.indexName,
query: { /* query */ },
from: (page - 1) * size,
size,
});
return {
results: response.hits.hits.map(hit => hit._source),
total: response.hits.total.value,
page,
pageSize: size,
};
}
Index Mapping Best Practices
MUST define appropriate field mappings:
mappings: {
properties: {
// Use "text" for full-text search fields
firstName: { type: "text" },
lastName: { type: "text" },
// Use "keyword" for exact match fields
email: { type: "keyword" },
status: { type: "keyword" },
// Use "date" for timestamp fields
createdAt: { type: "date" },
// Use "long" for numeric fields
age: { type: "long" },
},
}
What's inside
14 sections covering client setup, index classes, queries, CRUD, error handling, sync, env vars, and mappings with code examples.
Change this for your project
- Replace
@/domain/entities/Customerwith your own entity import path - Replace
@/domain/search/SearchIndexwith your own interface path - Replace
customer.id,customer.firstName, etc. with your entity's fields - Replace
https://your-elasticsearch-cluster.comwith your cluster URL
Where it goes
Save in docs/ or the repository root. Gives agents and new contributors a map of the codebase.
Worth borrowing
- Dedicated search index classes per entity implementing a domain interface
- Sync database operations with search index in the service layer
- Consistent paginated search response format with total count
Related Documents
PCB Design Sources for Industry-Scale Testing
Lists GitHub repos and communities where you can download real PCB designs for testing optimization at scale.
Design Document: BharatSeva AI
Describes a 10-agent AWS system that helps India's informal workers access government schemes via voice-first, serverless architecture.
OpenClaw Enterprise Transformation Plan
Transforms a single-user AI agent into a dual-mode platform supporting both viral open-source and Fortune 500 enterprise deployments through phased security, IAM, audit, multi-tenancy, and Kubernetes features.
Qwen Image and Edit: Open-sourcing and Local GGUF Generations with Lightning
Documents the Qwen-Image and Qwen-Image-Edit models, covering architecture, training, benchmarks, ComfyUI setup, and prompting techniques for local GGUF deployment.