Skip to main content

Elasticsearch (vania_elasticsearch)

The vania_elasticsearch package provides a client for Elasticsearch with index management, a fluent query builder, bulk operations, and a Searchable model mixin.

Installation

dependencies:
vania_elasticsearch: ^1.0.0

Configuration

Add Elasticsearch settings to .env:

ELASTICSEARCH_HOST=localhost
ELASTICSEARCH_PORT=9200
ELASTICSEARCH_INDEX_PREFIX=myapp_

Register the service provider:

'providers': [
RouteServiceProvider(),
ElasticsearchServiceProvider(),
],

Or configure manually:

Elasticsearch.configure(
config: ElasticsearchConfig(
hosts: ['http://localhost:9200'],
defaultIndex: 'myapp',
indexPrefix: 'myapp_',
username: 'elastic',
password: 'changeme',
),
);

Index Management

import 'package:vania_elasticsearch/vania_elasticsearch.dart';

var index = Elasticsearch.index('products');

// Create an index with mapping
await index.create({
'mappings': {
'properties': {
'name': {'type': 'text'},
'price': {'type': 'float'},
'category': {'type': 'keyword'},
'description': {'type': 'text'},
'created_at': {'type': 'date'},
},
},
});

// Check if index exists
bool exists = await index.exists();

// Delete an index
await index.delete();

// Refresh (make recent changes searchable)
await index.refresh();

Indexing Documents

var index = Elasticsearch.index('products');

// Index a single document
await index.document('product_1', {
'name': 'Dart T-Shirt',
'price': 29.99,
'category': 'apparel',
'description': 'A comfortable t-shirt for Dart developers.',
});

// Check if a document exists
bool exists = await index.has('product_1');

// Get a document
var doc = await index.get('product_1');

// Update a document
await index.update('product_1', {'price': 24.99});

// Delete a document
await index.remove('product_1');

Searching

var results = await Elasticsearch.search(
index: 'products',
query: 'dart shirt',
);

Query Builder

Build complex queries with a fluent API:

var builder = ElasticsearchQueryBuilder()
.match('name', 'dart')
.filter({'range': {'price': {'lte': 50}}})
.sort('price', order: 'asc')
.from(0)
.size(20)
.highlight({'fields': {'name': {}, 'description': {}}});

var results = await Elasticsearch.searchResult(
index: 'products',
body: builder.build(),
);

print('Total: ${results.total}');
for (var hit in results.hits) {
print('${hit['_source']['name']} - \$${hit['_source']['price']}');
}

Bool Query

var builder = ElasticsearchQueryBuilder()
.must({'match': {'category': 'apparel'}})
.must({'range': {'price': {'gte': 10, 'lte': 100}}})
.should({'match': {'description': 'comfortable'}})
.mustNot({'term': {'status': 'discontinued'}})
.minimumShouldMatch(1);

Advanced Queries

// Term query (exact match)
var builder = ElasticsearchQueryBuilder().term('category', 'apparel');

// Terms query (match any)
var builder = ElasticsearchQueryBuilder().terms('category', ['apparel', 'accessories']);

// Range query
var builder = ElasticsearchQueryBuilder().range('price', gte: 10, lte: 100);

// Exists query
var builder = ElasticsearchQueryBuilder().exists('discount');

// Multi-match
var builder = ElasticsearchQueryBuilder().multiMatch('dart framework', ['name', 'description']);

// Match all
var builder = ElasticsearchQueryBuilder().matchAll();

Aggregations

var builder = ElasticsearchQueryBuilder()
.size(0)
.aggregation('avg_price', {'avg': {'field': 'price'}})
.aggregation('categories', {'terms': {'field': 'category'}});

var results = await Elasticsearch.searchResult(
index: 'products',
body: builder.build(),
);

print('Average price: ${results.aggregations['avg_price']['value']}');

Bulk Operations

await Elasticsearch.bulk([
ElasticsearchBulkOperation.index('product_1', {'name': 'Item 1', 'price': 10}),
ElasticsearchBulkOperation.index('product_2', {'name': 'Item 2', 'price': 20}),
ElasticsearchBulkOperation.update('product_3', {'price': 15}),
ElasticsearchBulkOperation.delete('product_old'),
], index: 'products');

Searchable Model Mixin

Add full-text search to your ORM models:

class Product extends Model with Searchable {
@override
List<String> get fillable => ['name', 'price', 'category', 'description'];

@override
String searchableAs() => 'products';

@override
Map<String, dynamic> toSearchable() => {
'name': getAttribute('name'),
'price': getAttribute('price'),
'category': getAttribute('category'),
'description': getAttribute('description'),
};
}

Then sync your model data with Elasticsearch:

// Index a single model
var product = await Product().query.find(1);
await product?.searchable();

// Remove from index
await product?.unsearchable();

// Search through the model
var results = await Product().search('dart shirt', size: 20);

// Bulk index all products
var products = await Product().query.get();
await bulkIndex<Product>(products);

Connection Health

bool alive = await Elasticsearch.ping();
var info = await Elasticsearch.info();