MeiliSearch 是一个开源、速度极快且高度相关的搜索引擎。但它不仅仅是通用搜索引擎:MeiliSearch 高度可定制的搜索引擎 API 为您提供了极大的灵活性。例如,您可以修改排名规则、添加自定义排名规则、配置同义词、过滤掉停用词等等。为了提高您的搜索能力,MeiliSearch 允许您设置作为索引的分面过滤器。
背景
因为最近开发文档搜索功能,所以开始接触搜索引擎,目的主要是对比和选型。
搜索引擎有很多,最为常见的是ES
,是搜索引擎的主流,但ES太重了,对于小项目而言增加的硬件成本,运维成本。对于小项目来说不太合适。想要分词很好,要么费人,要么费机器。
至于利用关系型数据库
(MySQL和PostgreSQL等)自带的全文检索功能,虽然有对应的功能,但性能不佳,很费机器。
综合研究下来,开箱即用的 MeiliSearch, 对于中小型项目来说,MeiliSearch 是一个不错的选择。
使用教程
安装Meilisearch服务
代码语言:javascript复制# Install Meilisearch
curl -L https://install.meilisearch.com | sh
# Launch Meilisearch
./meilisearch
验证
查看索引列表
代码语言:javascript复制curl http://127.0.0.1:7700/indexes
// {"results":[],"offset":0,"limit":20,"total":0}
// 因为是从零开始的,所以索引列表应该是空数组
设置访问密码
可选,为了安全还是要设置的
代码语言:javascript复制./meilisearch --master-key="MASTER_KEY"
// 访问的时候要带上密码
curl http://127.0.0.1:7700/indexes / --header 'Authorization: Bearer xxxx'
// 如果是0.24以下老版本,--header 'X-Meili-API-Key: xxxx'
现在我们可以愉快的尝试meiliSearch的强大功能了
初始化客户端
首先,创建用npm创建新项目:
代码语言:javascript复制mkdir myMeiliSearch
cd ./myMeiliSearch
npm init -y
然后,安装meilisearch-js 依赖:
代码语言:javascript复制npm install meilisearch
最终, 创建一个index.js 文件在根目录,在这里输入我们的代码。
代码语言:javascript复制touch index.js
填充一些demo数据
数据字段如下
代码语言:javascript复制{
id: 'number',
title: 'string',
genres: 'Array'
}
index.js如下
代码语言:javascript复制const {MeiliSearch} = require('meilisearch')
const main = async () => {
const client = new MeiliSearch({
host: 'http://127.0.0.1:7700',
apiKey: 'xxxx',
})
// An index is where the documents are stored.
const index = client.index('movies')
const documents = [
{ id: 1, title: 'Carol', genres: ['Romance', 'Drama'] },
{ id: 2, title: 'Wonder Woman', genres: ['Action', 'Adventure'] },
{ id: 3, title: 'Life of Pi', genres: ['Adventure', 'Drama'] },
{ id: 4, title: 'Mad Max: Fury Road', genres: ['Adventure', 'Science Fiction'] },
{ id: 5, title: 'Moana', genres: ['Fantasy', 'Action']},
{ id: 6, title: 'Philadelphia', genres: ['Drama'] },
]
// If the index 'movies' does not exist, Meilisearch creates it when you first add the documents.
let response = await index.addDocuments(documents)
console.log(response) // => { "uid": 0 }
// 打印效果如下
/*EnqueuedTask {
taskUid: 0,
indexUid: 'movies',
status: 'enqueued',
type: 'documentAdditionOrUpdate',
enqueuedAt: 2022-11-02T07:20:47.332Z
}*/
}
main()
诸如文档添加之类的任务总是返回一个唯一的标识符。您可以使用此标识符 taskUid 来检查任务的状态(入队、处理、成功或失败)。
查询
index.js 代码如下
代码语言:javascript复制const { MeiliSearch } = require('meilisearch')
const main = async () => {
const client = new MeiliSearch({
host: 'http://127.0.0.1:7700',
apiKey: 'xxxx',
})
// An index is where the documents are stored.
const index = client.index('movies')
// Meilisearch is typo-tolerant:
const search = await index.search('philoudelphia')
console.log(search)
// 查询结果如下
/*
{
hits: [ { id: 6, title: 'Philadelphia', genres: [Array] } ],
estimatedTotalHits: 1,
query: 'philoudelphia',
limit: 20,
offset: 0,
processingTimeMs: 4
}
*/
}
同时尝试,文本前缀
触发
const search = await index.search('phi')
// 查询结果如下
/*
{
hits: [ { id: 6, title: 'Philadelphia', genres: [Array] } ],
estimatedTotalHits: 1,
query: 'phi',
limit: 20,
offset: 0,
processingTimeMs: 0
}
*/
发现依然有效,当如如果是中间的单词。比如Philadelphia的中间部分ladel,这时候有query:ladel
,是搜出不来的。
那么 Road 这种明显是分隔的单词(分词
)怎么样,我尝试了一些query:Max
和query:Road
的时候,都可以。
重点来了,中文支持的如何呢?
我们继续往索引中添加数据,
代码语言:javascript复制[
{ id: 7, title: '一只狐狸', genres: ['狐狸', '老爷爷'] },
{ id: 8, title: '帅气的猫', genres: ['蓝色的猫', '淘气的老鼠'] },
{ id: 9, title: '喜欢盆子的狗', genres: ['盆子', '狗'] },
{ id: 10, title: '无敌的超人', genres: ['超人', '可路菲菲'] },
{ id: 11, title: '斗破天下', genres: ['萧快乐', '范无敌'] },
{ id: 12, title: '闺蜜之主', genres: ['赵猥琐', '美丽女神'] },
]
然后,我们搜索一下“猫”,不负众望,
代码语言:javascript复制{
hits: [ { id: 8, title: '帅气的猫', genres: [Array] } ],
estimatedTotalHits: 1,
query: '猫',
limit: 20,
offset: 0,
processingTimeMs: 0
}
无论是搜单个字还是全部,它都可以匹配出来,简直完美。仅这些,使用es实现就要好几天,甚至不能轻易实现。
条件判断
如果你需要条件判断,那么你需要先配置一下
代码语言:javascript复制// 官网上的ReadMe.md index.updateAttributesForFaceting 是过期的写法
//详情 https://github.com/meilisearch/meilisearch-js/pull/941
await index.updateFilterableAttributes([
'id',
'genres'
])
如果你还想搜索genres字段,你需要
代码语言:javascript复制await index.updateSearchableAttributes([
'id',
'title',
'genres'
])
查询一下id大于3且genres中包含Adventure的项
代码语言:javascript复制const search = await index.search(
'Adventure',
{
filter: ['id > 3']
}
)
console.log(search)
/*
{
hits: [ { id: 4, title: 'Mad Max: Fury Road', genres: [Array] } ],
estimatedTotalHits: 1,
query: 'Adventure',
limit: 20,
offset: 0,
processingTimeMs: 0
}
*/
是不是很容易?
附上完整代码
代码语言:javascript复制const { MeiliSearch } = require('meilisearch')
const main = async () => {
const client = new MeiliSearch({
host: 'http://127.0.0.1:7700',
apiKey: 'xxxx',
})
// An index is where the documents are stored.
const index = client.index('movies')
await index.updateFilterableAttributes([
'id',
'genres'
])
await index.updateSearchableAttributes([
'id',
'title',
'genres'
])
// addDocument(index)
// addDocumentZh(index)
// addDocumentEnZh(index)
// updateDocumentEnZh(index)
// Meilisearch is typo-tolerant:
// const search = await index.search('ww')
const search = await index.search(
'Adventure',
{
filter: ['id > 3']
}
)
console.log(search)
}
async function addDocument(index) {
const documents = [
{ id: 1, title: 'Carol', genres: ['Romance', 'Drama'] },
{ id: 2, title: 'Wonder Woman', genres: ['Action', 'Adventure'] },
{ id: 3, title: 'Life of Pi', genres: ['Adventure', 'Drama'] },
{ id: 4, title: 'Mad Max: Fury Road', genres: ['Adventure', 'Science Fiction'] },
{ id: 5, title: 'Moana', genres: ['Fantasy', 'Action'] },
{ id: 6, title: 'Philadelphia', genres: ['Drama'] },
]
// If the index 'movies' does not exist, Meilisearch creates it when you first add the documents.
let response = await index.addDocuments(documents)
console.log(response) // => { "uid": 0 }
}
async function addDocumentZh(index) {
const documents = [
{ id: 7, title: '一只狐狸', genres: ['狐狸', '老爷爷'] },
{ id: 8, title: '帅气的猫', genres: ['蓝色的猫', '淘气的老鼠'] },
{ id: 9, title: '喜欢盆子的狗', genres: ['盆子', '狗'] },
{ id: 10, title: '无敌的超人', genres: ['超人', '可路菲菲'] },
{ id: 11, title: '斗破天下', genres: ['萧快乐', '范无敌'] },
{ id: 12, title: '闺蜜之主', genres: ['赵猥琐', '美丽女神'] },
]
// If the index 'movies' does not exist, Meilisearch creates it when you first add the documents.
let response = await index.addDocuments(documents)
console.log(response) // => { "uid": 0 }
}
async function addDocumentEnZh(index) {
const documents = [
{ id: 13, title: '一只App狐狸', genres: ['狐狸', '老爷爷'] },
{ id: 14, title: 'Niu帅气的猫', genres: ['蓝色的猫', '淘气的老鼠'] },
{ id: 15, title: '喜欢盆子A的狗', genres: ['盆子', '狗'] },
{ id: 16, title: '无敌的超人', genres: ['超人', '可路菲菲'] },
{ id: 17, title: '斗Kankan破K天下', genres: ['萧快乐', '范无敌'] },
{ id: 18, title: '闺蜜Www之主', genres: ['赵猥琐', '美丽女神'] },
]
// If the index 'movies' does not exist, Meilisearch creates it when you first add the documents.
let response = await index.addDocuments(documents)
console.log(response) // => { "uid": 0 }
}
async function updateDocumentEnZh(index) {
const documents = [
{ id: 13, title: '一只App狐狸', genres: ['狐狸', '老爷爷'] },
{ id: 14, title: 'Niu帅气的猫', genres: ['蓝色的猫', '淘气的老鼠'] },
{ id: 15, title: '喜欢盆子A的狗', genres: ['盆子', '狗'] },
{ id: 16, title: '无敌的超人', genres: ['超人', '可路菲菲'] },
{ id: 17, title: '斗Kankan破K天下', genres: ['萧快乐', '范无敌'] },
{ id: 18, title: '闺蜜Www之主1', genres: ['赵猥琐', '美丽女神'] },
]
// If the index 'movies' does not exist, Meilisearch creates it when you first add the documents.
let response = await index.updateDocuments(documents)
console.log(response) // => { "uid": 0 }
}
main()