@isomx/meilisearch-index-mgr
v4.3.7
Published
A helper around the Meilisearch JavaScript client that makes it easier to drive rich, reactive search UIs. It wraps a single Meilisearch Index with an RxJS-powered queue, optional debouncing, easy in-flight cancellation, and an optional local cache of hit
Readme
Summary
A helper around the Meilisearch JavaScript client that makes it easier to drive rich, reactive search UIs. It wraps a single Meilisearch Index with an RxJS-powered queue, optional debouncing, easy in-flight cancellation, and an optional local cache of hits for instant access later.
Features
- Observable stream of results you can subscribe to from any component
- Optional debouncing for fast, low-latency typing experiences
- Auto-abandon in-flight requests when a new one is queued
- Simple local cache of hits with configurable cache key and case-normalization
- "Load more" helper that appends results to your existing hits
Installation
- Peer dependency: meilisearch ^0.54
- Dependency: rxjs ^6.6
Use your package manager of choice:
- npm: npm i @isomorix/meilisearch-index-mgr
- yarn: yarn add @isomorix/meilisearch-index-mgr
- pnpm: pnpm add @isomorix/meilisearch-index-mgr
Quick start
- Create a Meilisearch client and index
- Create an IndexMgr instance
- Subscribe to results and/or fire requests
Example
import { MeiliSearch } from 'meilisearch';
import { IndexMgr } from '@isomorix/meilisearch-index-mgr';
const client = new MeiliSearch({ host: 'http://127.0.0.1:7700' });
const index = client.index('movies');
const mgr = new IndexMgr(index, {
debounce: 200,
autoAbandonInFlight: true,
cache: true,
cacheKey: 'id',
lowerCaseCacheKeys: false,
});
// Render from anywhere by subscribing
const sub = mgr.subscribe(({ resp, query, error }) => {
if (error) {
console.error('Search failed', error);
return;
}
if (!resp) return;
console.log('Results for', query, resp.hits);
});
// Fire a request from a different component (no subscription required here)
mgr.next({ query: 'batman', options: { limit: 10 } });
// Later, when done
sub.unsubscribe();
mgr.complete();Common UI patterns
Debounced search input (vanilla JS)
const input = document.querySelector('#search');
input.addEventListener('input', (e) => {
mgr.next({ query: e.target.value, options: { limit: 5 } });
});
// Debouncing is handled by the mgr options.Autocomplete dropdown (vanilla JS)
- Subscribe to mgr and render a dropdown under your input
- Use
payload.options.attributesToHighlightand highlight pre/post tags to style matches
Example:
const dropdown = document.querySelector('#ac');
const acSub = mgr.subscribe(({ resp }) => {
if (!resp) return;
dropdown.innerHTML = resp.hits.map(h => `<div class="item">${h.title}</div>`).join('');
dropdown.hidden = resp.hits.length === 0;
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') dropdown.hidden = true;
});Load more / infinite scroll
- The response is enriched with
payload.hasMoreandpayload.getMore(limit?), also available onpayload.resp.hasMoreandpayload.resp.getMore(limit?). - Call
payload.getMore()(orpayload.resp.getMore()) to fetch the next page. Returned hits are appended topayload.resp.hitsand a new Array is set on the providedpayloadcontaining all results.
Example:
const loadMoreBtn = document.querySelector('#loadMore');
const listSub = mgr.subscribe(({ resp }) => {
if (!resp) return;
renderList(resp.hits);
loadMoreBtn.disabled = !resp.hasMore;
});
loadMoreBtn.addEventListener('click', () => {
const current = mgr.getResp();
if (current && current.getMore) {
// Optionally pass a new limit for the next page
current.getMore(20).subscribe();
}
});Using the local cache
- Enable cache with cache: true (or provide your own object)
- Choose a cacheKey that uniquely identifies a document (e.g., id or slug)
- Retrieve items quickly without hitting the server: mgr.getFromCache(key)
Example:
const hit = mgr.getFromCache('movie-123');
if (hit) {
// Render instantly while you also kick off a background refresh
}Abandoning requests (e.g., fast typing)
- Use mgr.get(payload) if you want an observable you can unsubscribe to cancel
- Or set autoAbandonInFlight: true and use mgr.next(payload) to auto-cancel previous requests
Example:
const subReq = mgr.get({ query: 'super', options: { limit: 5 } }).subscribe(({ resp }) => {
// Handle results
});
// Cancel in-flight
subReq.unsubscribe();React example (functional)
import React, { useEffect, useMemo, useState } from 'react';
import { MeiliSearch } from 'meilisearch';
import { IndexMgr } from '@isomorix/meilisearch-index-mgr';
export function MovieSearch() {
const [hits, setHits] = useState([]);
const [query, setQuery] = useState('');
const mgr = useMemo(() => {
const client = new MeiliSearch({ host: 'http://127.0.0.1:7700' });
return new IndexMgr(client.index('movies'), { debounce: 200, autoAbandonInFlight: true });
}, []);
useEffect(() => {
const sub = mgr.subscribe(({ resp }) => {
if (resp) setHits(resp.hits);
});
return () => { sub.unsubscribe(); mgr.complete(); };
}, [mgr]);
useEffect(() => { mgr.next({ query, options: { limit: 8 } }); }, [mgr, query]);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />
<ul>{hits.map(h => <li key={h.id}>{h.title}</li>)}</ul>
</div>
);
}Vue 3 example
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { MeiliSearch } from 'meilisearch';
import { IndexMgr } from '@isomorix/meilisearch-index-mgr';
const query = ref('');
const hits = ref([]);
let mgr;
onMounted(() => {
const client = new MeiliSearch({ host: 'http://127.0.0.1:7700' });
mgr = new IndexMgr(client.index('movies'), { debounce: 200, autoAbandonInFlight: true });
const sub = mgr.subscribe(({ resp }) => { if (resp) hits.value = resp.hits; });
// store subscription if you need to unsubscribe later
});
function onInput(e) {
mgr.next({ query: e.target.value, options: { limit: 8 } });
}
onBeforeUnmount(() => { mgr?.complete(); });
</script>
<template>
<input :value="query" @input="onInput" placeholder="Search movies" />
<ul>
<li v-for="h in hits" :key="h.id">{{ h.title }}</li>
</ul>
</template>API summary
IndexMgr constructor
new IndexMgr(index: Meili Index, options?: IndexMgrOptions)
- index:
client.index('name')from meilisearch - options:
debounce?: number— milliseconds to delay before running the latest queued requestcache?: true | object— enable caching of hits into the objectcacheKey?: string— property name on each hit used as the cache keylowerCaseCacheKeys?: boolean— normalize keys to lowercasepreparePayload?: (payload) => payload— hook to modify payloads before executionautoAbandonInFlight?: boolean— cancel in-flight request when next() queues a new one
IndexMgr methods
get(payload): Observable— queue a request and return an observable that emits the payload when done. Unsubscribe to cancel in-flight.next(payload): void— queue a request without returning an observable. Results are emitted to subscribers of the IndexMgr instance.execute(payload): Observable— run one-off request without touching cache or notifying subscribers.subscribe(observerOrNext?): Subscription | Observable— subscribe to payloads with results; if no arg, returns the observable stream.reset(): IndexMgr— clears cache (if enabled) and last value.complete(): void— dispose and complete internal streams.getValue(): payload | null— last completed payload.getResp(): response | undefined— last completed response.getFromCache(key: string): object | undefined— retrieve a cached hit.
Payload (what you pass to get()/next()/execute())
query?: string— search queryoptions?: SearchParams— meilisearch search optionsprevHits?: any[]— when using getMore internally, previous hits are merged with the next pageresp?: SearchResponse— set on completionhasMore?: boolean— indicates if more results are availablegetMore?: (limit?: number) => Observable— helper to fetch next page and append toresp.hitserror?: any— set if the search fails
TypeScript support
- This package ships types at types/index.d.ts.
- After installing, editors/TS should resolve types automatically.
- You can parameterize the document shape:
IndexMgr<MyDoc>
Example:
type Movie = { id: string; title: string; };
const mgr = new IndexMgr<Movie>(index, { cache: true, cacheKey: 'id' });
const sub = mgr.subscribe(({ resp }) => {
const titles: string[] = (resp?.hits ?? []).map(h => h.title);
});Best practices and tips
- Use
debounceoption to debounce typing to reduce load and avoid flicker;150–300ms is common. - Use
autoAbandonInFlightin UIs where a new keystroke makes prior results irrelevant. - Keep cacheKey stable (id/uuid/slug) if you enable caching; combine with
lowerCaseCacheKeysfor human-entered keys. - Show loading states by tracking the time between queuing and receiving a response.
- Use
getMore()to build infinite scroll; it automatically appends to resp.hits. - Always clean up:
unsubscribe()subscriptions you own and callindexMgr.complete()when disposing the IndexMgr.
Error handling
- Subscribe to errors by inspecting
payload.errorin your subscription callback. - Network cancellations (AbortError) when you cancel are expected; consider ignoring those in UI.
SSR notes
- This library is UI/runtime focused. If using with SSR, instantiate and dispose inside request or per-view lifecycles to avoid leaking subscriptions.
