searchbase 4.0.0-alpha.6 searchbase: ^4.0.0-alpha.6 copied to clipboard
SearchBase is a library to build the search UIs with Elasticsearch.
SearchBase #
searchbase is a lightweight and platform agnostic library that provides scaffolding to create search experiences powered by Elasticsearch.
Example #
A simple example #
The following example creates a single widget of type(search
) to render the results based on the input value.
import 'dart:html';
import 'package:searchbase/searchbase.dart';
void main() {
final index = 'gitxplore-app';
final url = 'https://@appbase-demo-ansible-abxiydt-arc.searchbase.io';
final credentials = 'a03a1cb71321:75b6603d-9456-4a5a-af6b-a487b309eb61';
// Instantiate the [SearchController]
final searchController = SearchController(
// Elasticsearch index name
index,
// Appbase URL
url,
// Appbase credentials
credentials,
// Unique identifier for search widget
'search-widget',
// Database fields to perform the search
dataField: ['name', 'description', 'name.search', 'fullname', 'owner', 'topics'],
// initial value
value: ''
);
// Get the input element
final searchElement = querySelector('#search');
// Bind the searchController value to input value
searchElement.value = searchController.value;
// Update the search input value to searchController to fetch the results
searchElement.addEventListener('input', (e) {
// To fetch the suggestions based on the value changes
searchController.setValue(e.target.value,
options: Options(triggerDefaultQuery: true));
});
// Build DOM when search results update
searchController.subscribeToStateChanges((change) {
final results = change.Results!.next;
final resultsElement = querySelector('#results');
resultsElement.innerHTML = '';
results.data.forEach((element) {
var node = document.createElement('li'); // Create a <li> node
var resultNode = document.createTextNode(element.name); // Create a text node
node.append(resultNode); // Append the text to <li>
resultsElement.append(node);
});
},
[KeysToSubscribe.Results],
);
// Fetch the default results at initial load
searchController.triggerDefaultQuery();
}
Add this in your HTML
<input placeholder="type to search" id="search" />
<div id="results"></div>
An example with a facet #
The following example creates three widgets:
- a search widget to perform the search,
- a filter widget to filter the GitHub repo by languages,
- a result widget to render the results based on the selected language filters and search query
The result widget watches for changes to the search and language filter widgets (see the react
property). It reacts to the inputs and filter selection changes by triggering an Elasticsearch
query to update results.
The language filter widget is also watching for changes to the search. For example, if somebody searches for angular
then the language filter will show javascript
as an option.
Note: This example is using the
SearchBase
class instead of theSearchController
(that we used in the previous example) class because here we're using multiple widgets that can have dependencies on each other.
import 'dart:html';
import 'package:searchbase/searchbase.dart';
void main() {
final index = 'gitxplore-app';
final url = 'https://@appbase-demo-ansible-abxiydt-arc.searchbase.io';
final credentials = 'a03a1cb71321:75b6603d-9456-4a5a-af6b-a487b309eb61';
final searchbase = SearchBase(index, url, credentials,
appbaseConfig: AppbaseSettings(recordAnalytics: true));
// Register search widget => To render the suggestions
final searchController = searchbase.register('search-widget', {
'enablePopularSuggestions': true,
'dataField': [
'name',
'description',
'name.raw',
'fullname',
'owner',
'topics'
]
});
// Register filter widget with dependency on search widget
final filterWidget = searchbase.register('language-filter', {
'type': QueryType.term,
'dataField': 'language.keyword',
'react': {'and': 'search-widget'},
'value': List<String>()
});
// Register result widget with react dependency on search and filter widget => To render the results
final resultWidget = searchbase.register('result-widget', {
'dataField': 'name',
'react': {
'and': ['search-widget', 'language-filter']
},
});
// Render results
querySelector('#output').innerHtml = """
<div id="root">
<h2 class="text-center">Searchbase Demo with Facet</h2>
<div id="autocomplete" class="autocomplete">
<input class="autocomplete-input" id="input" />
<ul class="autocomplete-result-list"></ul>
</div>
<div class="row">
<div class="col">
<div class="filter" id="language-filter"></div>
</div>
<div class="col">
<div id="results">
<div class="loading">Loading results... </div>
</div>
</div>
</div>
</div>
""";
final input = querySelector('#input');
void handleInput(e) {
// Set the value to fetch the suggestions
searchController.setValue(e.target.value,
options: Options(triggerDefaultQuery: true));
}
input.addEventListener('input', handleInput);
void handleKeyPress(e) {
// Fetch the results
if (e.key == 'Enter') {
e.preventDefault();
searchController.triggerCustomQuery();
}
}
input.addEventListener('keydown', handleKeyPress);
final resultElement = querySelector('#results');
// Fetch initial results
resultWidget.triggerDefaultQuery();
// subscribe to `results` property to update re-build result list when update happens
resultWidget.subscribeToStateChanges((change) {
final results = change.Results!.next;
final items = results.data?.map((i) {
return """
<div id=${i['_id']} class="result-set">
<div class="image">
<img src=${i['avatar']} alt=${i['name']} />
</div>
<div class="details">
<h4>${i['name']}</h4>
<p>${i['description']}</p>
</div>
</div>""";
});
final resultStats = """<p class="results-stats">
Showing ${results.numberOfResults} in ${results.time}ms
<p>""";
resultElement.setInnerHtml("${resultStats}${items.join('')}",
validator: new NodeValidatorBuilder.common()
..allowHtml5()
..allowElement('img',
attributes: ['src'], uriPolicy: new DefaultUriPolicy()));
}, [KeysToSubscribe.Results]);
// Fetch initial filter options
filterWidget.triggerDefaultQuery();
// subscribe to updates in `aggregationData` property so filter options can change based on search
filterWidget.subscribeToStateChanges((change) {
final aggregations = change.AggregationData!.next;
final container = document.getElementById('language-filter');
container.setInnerHtml('');
aggregations.data.forEach((i) {
if (i['_key'] != null) {
final checkbox = document.createElement('input');
checkbox.setAttribute('type', 'checkbox');
checkbox.setAttribute('name', i['_key']);
checkbox.setAttribute('value', i['_key']);
checkbox.id = i['_key'];
checkbox.addEventListener('click', (event) {
final List<String> values =
filterWidget.value != null ? filterWidget.value : [];
if (values.contains(i['_key'])) {
values.remove(i['_key']);
} else {
values.add(i['_key']);
}
// Set filter value and trigger custom query
filterWidget.setValue(values,
options: Options(stateChanges: true, triggerCustomQuery: true));
});
final label = document.createElement('label');
label.setAttribute('htmlFor', 'i._key');
label.setInnerHtml("${i['_key']}(${i['_doc_count']})");
final div = document.createElement('div');
div.append(checkbox);
div.append(label);
container.append(div);
}
});
}, [KeysToSubscribe.AggregationData]);
searchController.subscribeToStateChanges((change) {
print('Track State Updates');
print(change);
}, [KeysToSubscribe.Results]);
}
class DefaultUriPolicy implements UriPolicy {
DefaultUriPolicy();
bool allowsUri(String uri) {
return true;
}
}
Facet example extended to use SearchStateController to handle state changes side-effects #
This example is a version of the previous example which uses SearchStateController
class to watch for state changes and perform relevant side-effects.
import 'dart:html';
import 'package:searchbase/searchbase.dart';
bool areListsEqual(var list1, var list2) {
// check if both are lists
if (!(list1 is List && list2 is List)
// check if both have same length
||
list1.length != list2.length) {
return false;
}
// check if elements are equal
for (int i = 0; i < list1.length; i++) {
if (list1[i] != list2[i]) {
return false;
}
}
return true;
}
class DefaultUriPolicy implements UriPolicy {
DefaultUriPolicy();
@override
bool allowsUri(String uri) {
return true;
}
}
void main() {
final index = 'gitxplore-app';
final url = 'https://@appbase-demo-ansible-abxiydt-arc.searchbase.io';
final credentials = 'a03a1cb71321:75b6603d-9456-4a5a-af6b-a487b309eb61';
final searchbase = SearchBase(index, url, credentials,
appbaseConfig: AppbaseSettings(recordAnalytics: true));
// Register search widget => To render the suggestions
final searchController = searchbase.register('search-widget', {
'enablePopularSuggestions': true,
'dataField': [
'name',
'description',
'name.raw',
'fullname',
'owner',
'topics'
]
});
// Register filter widget with dependency on search widget
final filterWidget = searchbase.register('language-filter', {
'type': QueryType.term,
'dataField': 'language.keyword',
'react': {'and': 'search-widget'},
'value': ""
});
// Register result widget with react dependency on search and filter widget => To render the results
final resultWidget = searchbase.register('result-widget', {
'dataField': 'name',
'react': {
'and': ['search-widget', 'language-filter']
},
'defaultQuery': (SearchController controller) =>
({'track_total_hits': true})
});
// Render results
querySelector('#output')!.innerHtml = '''
<div id="root">
<h2 class="text-center">Searchbase Demo with Facet</h2>
<div id="autocomplete" class="autocomplete">
<input class="autocomplete-input" id="input" />
<ul class="autocomplete-result-list"></ul>
</div>
<div class="row">
<div class="col">
<div class="filter" id="language-filter"></div>
</div>
<div class="col">
<div id="results">
<div class="loading">Loading results... </div>
</div>
</div>
</div>
</div>
''';
final input = querySelector('#input');
void handleInput(e) {
// Set the value to fetch the suggestions
searchController.setValue(e.target.value,
options: Options(triggerDefaultQuery: true));
}
input!.addEventListener('input', handleInput);
void handleKeyPress(e) {
// Fetch the results
if (e.key == 'Enter') {
e.preventDefault();
searchController.triggerCustomQuery();
}
}
input.addEventListener('keydown', handleKeyPress);
final resultElement = querySelector('#results');
// Fetch initial results
resultWidget.triggerDefaultQuery();
// Fetch initial filter options
filterWidget.triggerDefaultQuery();
List<Map<dynamic, dynamic>>? prevAggData = [];
// Instance of `SearchStateController` class
// used for subscription to various properties for different widgets
// and relevant side-effects are performed
final searchStateController = SearchStateController(
searchBase: searchbase,
subscribeTo: {
'result-widget': [KeysToSubscribe.Results],
'language-filter': [KeysToSubscribe.AggregationData],
'search-widget': [KeysToSubscribe.Results],
},
onChange: (next, prev) {
// handle side-effects for state changes in result-widget
if (next['result-widget'] != null) {
print('result-widget subscribed');
final results = next['result-widget']!.results;
final items = results!.data.map((i) {
return """
<div id=${i['_id']} class="result-set">
<div class="image">
<img src=${i['avatar']} alt=${i['name']} />
</div>
<div class="details">
<h4>${i['name']}</h4>
<p>${i['description']}</p>
</div>
</div>""";
});
final resultStats = '''<p class="results-stats">
Showing ${results.numberOfResults} in ${results.time}ms
<p>''';
resultElement!.setInnerHtml("$resultStats${items.join('')}",
validator: NodeValidatorBuilder.common()
..allowHtml5()
..allowElement('img',
attributes: ['src'], uriPolicy: DefaultUriPolicy()));
}
// handle side-effects for state changes in language-filter
if (next['language-filter'] != null &&
areListsEqual(next['language-filter']!.aggregationData?.data,
prevAggData) ==
false) {
prevAggData = next['language-filter']!.aggregationData?.data;
print('language-widget subscribed');
final aggregations = next['language-filter']!.aggregationData;
print(' aggregations?.data ${aggregations?.data}');
final container = document.getElementById('language-filter');
container!.setInnerHtml('');
aggregations?.data?.forEach((i) {
if (i['_key'] != null) {
final checkbox = document.createElement('input');
checkbox.setAttribute('type', 'checkbox');
checkbox.setAttribute('name', i['_key']);
checkbox.setAttribute('value', i['_key']);
checkbox.id = i['_key'];
checkbox.addEventListener('click', (event) {
List values = [];
if (filterWidget.value != null && filterWidget.value != "") {
values = filterWidget.value;
}
if (values.contains(i['_key'])) {
values.remove(i['_key']);
} else {
values.add(i['_key']);
}
// Set filter value and trigger custom query
filterWidget.setValue(values,
options:
Options(stateChanges: true, triggerCustomQuery: true));
});
final label = document.createElement('label');
label.setAttribute('htmlFor', 'i._key');
label.setInnerHtml("${i['_key']}(${i['_doc_count']})");
final div = document.createElement('div');
div.append(checkbox);
div.append(label);
container.append(div);
}
});
}
// handle side-effects for state changes in search-widget
if (next['search-widget'] != null) {
print('search-widget subscribed');
print('PREV ${prev['search-widget']?.results?.numberOfResults}');
print('NEXT ${next['search-widget']?.results?.numberOfResults}');
}
},
);
}