nest-solr
v0.1.8
Published
NestJS module for Apache Solr with query builder
Readme
Installation
npm install nest-solrQuick start
Register the module synchronously:
import { Module } from '@nestjs/common';
import { SolrModule } from 'nest-solr';
@Module({
imports: [
SolrModule.forRoot({
host: 'localhost',
port: 8983,
core: 'mycore',
secure: false,
}),
],
})
export class AppModule {}Or asynchronously using a factory:
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { SolrModule } from 'nest-solr';
@Module({
imports: [
ConfigModule.forRoot(),
SolrModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
host: config.get<string>('SOLR_HOST', 'localhost'),
port: config.get<number>('SOLR_PORT', 8983),
core: config.get<string>('SOLR_CORE', 'mycore'),
secure: config.get<boolean>('SOLR_SECURE', false),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}Resilience options (timeouts, retries, circuit breaker)
You can optionally enable resilience behaviors on the client. All fields are optional and default to disabled, preserving previous behavior.
- timeoutMs: Request timeout in milliseconds. When exceeded, the request fails with a timeout error.
- retry: Controls automatic retries on failure.
- attempts: Number of retry attempts (default 0 = disabled).
- backoffMs: Base wait in ms between attempts; linear backoff is used: backoffMs × attemptNumber.
- breaker: Simple circuit breaker based on consecutive failures.
- threshold: Open the circuit after this many consecutive failures (default 0 = disabled). While open, requests fail fast until a short cooldown elapses.
// Synchronous configuration
SolrModule.forRoot({
host: 'localhost',
port: 8983,
core: 'mycore',
secure: false,
// Resilience (optional)
timeoutMs: 2000,
retry: { attempts: 2, backoffMs: 300 },
breaker: { threshold: 5 },
});// Async configuration
SolrModule.forRootAsync({
useFactory: () => ({
host: 'localhost',
port: 8983,
core: 'mycore',
secure: false,
// Resilience (optional)
timeoutMs: 2000,
retry: { attempts: 2, backoffMs: 300 },
breaker: { threshold: 5 },
}),
});Usage
import { Injectable } from '@nestjs/common';
import { SolrService } from 'nest-solr';
@Injectable()
export class SearchService {
constructor(private readonly solr: SolrService) {}
async find() {
const qb = this.solr.createQuery()
.eq('type', 'book')
.and()
.between('price', 10, 100)
.fields(['id', 'title', 'price'])
.sort('price', 'asc')
.rows(25);
return this.solr.search(qb.toParams());
}
}CRUD example
import { Injectable } from '@nestjs/common';
import { SolrService } from 'nest-solr';
interface Book {
id: string;
title: string;
price: number;
type: 'book';
}
@Injectable()
export class BooksService {
constructor(private readonly solr: SolrService) {}
// Create (single or bulk)
async createBooks() {
await this.solr.add<Book>(
{ id: 'b-1', title: 'Clean Code', price: 29.99, type: 'book' },
{ commitWithin: 500 },
);
await this.solr.add<Book>(
[
{ id: 'b-2', title: 'Node Patterns', price: 39.5, type: 'book' },
{ id: 'b-3', title: 'Mastering NestJS', price: 49, type: 'book' },
],
{ commitWithin: 500 },
);
// Or force an immediate commit for reads right away
await this.solr.commit();
}
// Read / Search
async findCheapBooks() {
const qb = this.solr
.createQuery()
.eq('type', 'book')
.and()
.lt('price', 40)
.fields(['id', 'title', 'price'])
.sort('price', 'asc')
.rows(10);
const result = await this.solr.search(qb);
return result.response?.docs ?? [];
}
// Update (same as add with overwrite)
async updatePrice(id: string, newPrice: number) {
await this.solr.add({ id, price: newPrice }, { overwrite: true });
await this.solr.commit();
}
// Delete
async removeById(id: string) {
await this.solr.deleteByID(id);
await this.solr.commit();
}
async removeByQuery() {
// Deletes all books cheaper than 10
await this.solr.deleteByQuery('type:book AND price:{* TO 10}');
await this.solr.commit();
}
}Advanced: complex query
import { Injectable } from '@nestjs/common';
import { SolrService } from 'nest-solr';
@Injectable()
export class SearchService {
constructor(private readonly solr: SolrService) {}
async complexSearch() {
const qb = this.solr
.createQuery()
// (type:book AND (title:"Mastering NestJS" OR title:Node*))
.group(true)
.eq('type', 'book')
.and()
.group(true)
.phrase('title', 'Mastering NestJS')
.or()
.startsWith('title', 'Node')
.group(false)
.group(false)
// AND category:(tech OR cooking)
.and()
.in('category', ['tech', 'cooking'])
// Filter price range
.filter('price:[10 TO 40]')
// Only return selected fields
.fields(['id', 'title', 'price'])
// Sort and paginate
.sort('price', 'desc')
.start(0)
.rows(10)
// Enable facets (example)
.facet(['category']);
// Execute
const result = await this.solr.search(qb);
return {
total: result.response?.numFound ?? 0,
items: result.response?.docs ?? [],
facets: result.facet_counts?.facet_fields ?? {},
};
}
}CursorMark pagination
import { Injectable } from '@nestjs/common';
import { SolrService } from 'nest-solr';
@Injectable()
export class CursorExampleService {
constructor(private readonly solr: SolrService) {}
// Fetch all books in pages of 50 using CursorMark
async fetchAllBooks() {
const qb = this.solr
.createQuery()
.eq('type', 'book')
// CursorMark requires deterministic sort; service will append `id asc` if missing
.sort('price', 'asc')
.rows(50)
.cursor('*'); // optional, you can also pass '*' to the service
let cursor = '*';
const all: any[] = [];
while (true) {
// You can either rely on qb.cursor('*') or pass the mark explicitly
const res = await this.solr.searchWithCursor(qb, cursor);
all.push(...(res.response?.docs ?? []));
if (!res.nextCursorMark || res.nextCursorMark === cursor) break; // end reached
cursor = res.nextCursorMark;
}
return all;
}
}Define schema (Schema API)
// Define field types, fields, copy fields and unique key
await solrService.defineSchema({
fieldTypes: [
{ name: 'text_en', class: 'solr.TextField', positionIncrementGap: '100' },
],
fields: [
{ name: 'id', type: 'string', stored: true, indexed: true, required: true },
{ name: 'title', type: 'text_en', stored: true, indexed: true },
],
// dest can be a string or string[]; multiple entries are expanded
copyFields: [{ source: 'title', dest: ['text', 'all'] }],
uniqueKey: 'id',
});SolrService API
- getClient(): returns the underlying HTTP client instance.
- createQuery(): creates a new
SolrQueryBuilderinstance. - add(docOrDocs, options?): adds a document or array of documents; supports
{ commitWithin, overwrite }. - commit(): commits pending changes (uses
waitSearcher=true). - deleteByID(id): deletes by
idfield. - deleteByQuery(query): deletes documents matching a query string (e.g.,
id:123). - optimize(options?): optimizes the index; supports
{ softCommit, waitSearcher, maxSegments }. - search(queryOrBuilder): executes a search; accepts a
SolrQueryBuilderor params object frombuilder.toParams(). - defineSchema(options): defines schema via Solr Schema API; supports
{ fieldTypes, fields, copyFields, uniqueKey }.
