Meilisearch client for Smalltalk. Currently, Pharo 11-13 and GemStone/S 3.6.x are supported.
Metacello new
baseline: 'Meilisearch';
repository: 'github://mumez/Meilisearch.st:main/src';
load.
To use Meilisearch, you need to set an API key. You can set the default API key at the system level.
MsSettings default apiKey: 'meili-api-key-A'.
After setting the default key, all newly created instances will use that API key.
meili := MeiliSearch new.
meili settings apiKey. "print it => 'meili-api-key-A'"
You can also explicitly set an API key on a per-instance basis:
meili := MeiliSearch apiKey: 'meili-api-key-B' url: 'http://localhost:7700'.
meili settings apiKey. "print it => 'meili-api-key-B'"
meili := MeiliSearch new.
task := meili createIndex: 'my-blog' primaryKey: 'id'.
"or just `meili createIndex: 'my-blog'.`"
task inspect. "You can see the task is enqueued"
resp := MeiliSearch new indexes.
resp results detect: [ :each | each uid = 'my-blog' ]. "print it =>
a MsIndex uid: 'my-blog' primaryKey: 'id' createdAt:
2023-06-26T07:10:18.44037373+00:00 updatedAt:
2023-06-26T07:10:18.458306996+00:00"
index := (MeiliSearch new index: 'my-blog') loaded.
index := MeiliSearch new index: 'my-blog'.
docs := {
{'id' -> 1. 'title' -> 'Woke up'. 'contents'->'I finally woke up. Started researching Meilisearch.' } asDictionary.
{'id' -> 2. 'title' -> 'Smalltalk'. 'contents'->'I did Smalltalk programming' } asDictionary.
{'id' -> 3. 'title' -> 'Meilisearch.st'. 'contents'->'I tried Meilisearch.st. Works good. I can add full-text search to my blog program in a few minutes.' } asDictionary.
}.
task := index putDocuments: docs.
task waitEndedForAWhile. "Await task is ended"
index := MeiliSearch new index: 'my-blog'.
resp := index search: 'Meilisearch'.
resp hits collect: [ :each | each at: 'id' ]. "print it => #(3 1)"
resp := index search: 'program'.
resp hits collect: [ :each | each at: 'id' ]. "print it => #(2 3)"
resp := index search: 'Smalltalk'.
resp hits. "print it => an Array(a Dictionary('contents'->'I did Smalltalk programming' 'id'->2
'title'->'Smalltalk' ))"
resp := index search: 'Meilisearch' optionsUsing:[:opts | opts attributesToRetrieve: #('id')].
resp hits. "print it => an Array(a Dictionary('id'->3 ) a Dictionary('id'->1 ))"
resp := index search: 'Meilisearch' optionsUsing:[:opts | opts attributesToRetrieve: #('id'); offset: 1; limit: 1].
resp hits. "print it => an Array(a Dictionary('id'->1 ))"
"You can apply index-specific settings for advanced searching"
attributes := #('id' 'title').
settingsTask := index applySettingsUsing: [ :opts |
opts sortableAttributes: attributes copy; filterableAttributes: attributes copy; displayedAttributes: attributes copy.
].
settingsTask waitEndedForAWhile.
resp := index search: 'Meilisearch' optionsUsing:[:opts | opts filter: 'title = "Woke up"'].
resp hits. "print it => an Array(a Dictionary('id'->1 'title'->'Woke up' ))"
You can also submit multiple searches in a single request.
(meili createIndex: 'my-wiki') waitEndedForAWhile.
otherIndex := meili index: 'my-wiki'.
otherIndex putDocuments: {
{'id' -> 1. 'title' -> 'Smalltalk meetup'. 'contents'->'June 9 will be a Smalltalk meet-up in Tokyo' } asDictionary.
}.
resp := meili multiSearchUsing: [ :opts | {
(opts index: otherIndex) q: 'Smalltalk'.
(opts index: 'my-blog') q: 'Smalltalk'; attributesToRetrieve: #('id')
}].
resp collect: [ :each | each hits ]. "print it => an Array(an Array(a Dictionary('contents'->'June 9 will be a Smalltalk meet-up
in Tokyo' 'id'->1 'title'->'Smalltalk meetup' )) an Array(a Dictionary('id'->2)))"
By setting #filterableAttributes: on an index, you can enable faceted search feature. The search response includes facets statistics, which can be used to further refine search results.
(meili createIndex: 'facet-books') waitEndedForAWhile.
booksIndex := meili index: 'facet-books'.
settingsTask := booksIndex applySettingsUsing: [ :opts |
opts filterableAttributes: #('title' 'rating' 'genres').
].
settingsTask waitEndedForAWhile.
docs := {
{'id' -> 1. 'title' -> 'Hard Times'. 'rating' -> 4.5.
'genres' -> #('Classics' 'Victorian' 'English Literature')} asDictionary.
{'id' -> 2. 'title' -> 'The Great Gatsby'. 'rating' -> 4.8.
'genres' -> #('Classics' 'American Literature' 'Romance') } asDictionary.
{'id' -> 3. 'title' -> 'Moby Dick'. 'rating' -> 4.7.
'genres' -> #('Classics' 'American Literature' 'Adventure') } asDictionary.
}.
(booksIndex putDocuments: docs) waitEndedForAWhile.
resp := booksIndex search: 'classic' optionsUsing: [:opts | opts facets: #('genres' 'rating')].
resp facetStats at: 'rating'. "print it => a Dictionary('max'->4.8 'min'->4.5 )"
resp facetDistribution at: 'genres'. "print it => a Dictionary('Adventure'->1 'American Literature'->2 'Classics'->3 'English
Literature'->1 'Romance'->1 'Victorian'->1 )"
resp := booksIndex search: 'America' optionsUsing: [:opts | opts facets: #('genres' 'rating')].
resp facetStats at: 'rating'. "print it => a Dictionary('max'->4.8 'min'->4.7 )"
resp facetDistribution at: 'genres'. "print it => a Dictionary('Adventure'->1 'American Literature'->2 'Classics'->2 'Romance'->1
)"
You can also use #facetSearchUsing: to search for facet values in the index. The #facetQuery: search word is a prefix match and allows typos.
resp := booksIndex facetSearchUsing: [:opts | opts facetQuery: 'clasic'; facetName: 'genres'; filter: 'rating > 4.5'].
facetHits := resp facetHits. "print it => an Array(a Dictionary('count'->2 'value'->'Classics' ))"
Meilisearch.st provides two types of pagination to handle large result sets efficiently:
- Offset Pagination - Uses
offset
andlimit
parameters - Numbered Page Pagination - Uses
page
andhitsPerPage
parameters
"Offset Pagination"
paginator1 := index offsetPaginator.
paginator1
search: 'English';
offset: 3; "Skip first 3 results"
limit: 5. "Return max 5 results"
"Numbered Page Pagination"
paginator2 := index numberedPagePaginator.
paginator2
search: 'English';
page: 1; "First page (1-based)"
hitsPerPage: 5. "5 results per page"
"Iterate through all results"
responses := OrderedCollection new.
[ paginator2 atEnd ] whileFalse: [
response := paginator2 next. "Get the search result"
responses add: response.
].
Meilisearch supports hybrid search, which combines keyword (lexical) and semantic (vector-based) search for more powerful and flexible results.
See MsIndex>>hybridSearch:query vector:vector embedder:embedderName semanticRatio:semanticRatio
and related methods for more details.
"Example of AI-powered hybrid search: combine text and vector queries"
resp := index hybridSearch: 'some words'
vector: #(1 2 3)
embedder: 'openAi-embedder'
semanticRatio: 0.5.
(MeiliSearch new index: 'my-blog') delete.