对于任何承载少量内容的网站来说,搜索是一个令人难以置信的重要方面。你的网站如何快速提供搜索结果,以及你的结果如何与用户的意图相匹配,是至关重要的。有多种搜索选项可供开发人员选择,因此有时为了追求速度和相关性,网站会从一种服务迁移到另一种服务。在这篇文章中,我将介绍一个用例,说明为什么一个网站会从Lunr这样的自我托管的解决方案迁移到Algolia这样的基于服务器的解决方案。我想强调的是,这并不意味着对Lunr的攻击,而更多的是它适合一个网站的地方,以及在我的特定用例中,转移到Algolia的好处是什么。
Lunr是如何工作的
Lunr是一个开源的基于JavaScript的搜索服务解决方案。使用Lunr,开发者首先创建一个他们希望能够搜索的内容的索引。这不需要对网站内容进行一对一的复制,而是可以进行定制,以专注于对搜索最有用的内容。这些数据可以来自任何地方,但必须以对象数组的形式提供给Lunr。让我们考虑一个有趣的假设数据集:可供收养的猫咪。他们的数据可能看起来像这样:
[
{
"name":"Fluffy",
"gender":"female",
"breed":"smelly",
"dob":"7/18/2022",
"history":"info about the cat...."
},
{
"name":"Muffy",
"gender":"female",
"breed":"not so smelly",
"dob":"2/18/2022",
"history":"info about the cat...."
}
]
看看这些数据,我们现在就可以做出执行决定,我们将让我们的用户按名字、性别、品种和历史进行搜索,但不按出生日期。随着Lunr库的加载,生成该索引的代码将看起来像这样:
let catReq = await fetch('./cats.json');
let cats = await catReq.json();
let index = lunr(function() {
this.ref('name');
this.field('gender');
this.field('breed');
this.field('history');
cats.forEach(function(c) {
this.add(c);
});
});
创建了索引后,搜索就变成了一行:
let results = index.search('something')
Lunr可能无法正常工作的地方
Lunr使用起来很简单,但也有几个需要注意的地方。这里有几件需要注意的事情,可能会使迁移更加理想:
-
首先,随着你的数据增长,你的索引也会增长。Lunr需要时间来创建索引,这个时间会随着被索引的内容的大小而增长。Lunr确实支持预先建立索引,但预先建立的索引仍然必须提供给用户。
-
另一个问题是,Lunr的结果只包含对原始数据的引用。因此,例如,在上面的猫的数据中,我们将引用定义为猫的名字。为了向用户返回一个有意义的结果,我们仍然需要使用这个引用来从我们的数据存储中获取关于猫的完整数据。
-
在服务器上使用Lunr是完全可能的,但这需要设置服务器,或无服务器函数。这些都是完全可能的,但开发者方面的工作更多。
-
Lunr有很多令人难以置信的功能,比如词干,但仍有一些缺席的必备功能,比如错别字处理和同义词匹配。另一个例子:我们怎样才能让Lunr将用户的位置纳入结果的相关性评分?
-
最后,也是最重要的一点:我们需要推出自己的分析功能。同样,这当然是可以做到的,但这将需要更多的代码,更多的数据存储,以及更多的维护。这些统计数字可能是非常重要的,因为它们反映了你的用户需要什么,以及他们在寻找什么方面遇到了困难。
迁移到Algolia
那么我们现在假设你已经确定你需要从Lunr迁移,那么迁移到Algolia是什么样子的呢?
首先,我们需要计算你的成本将是多少,如果有的话。Algolia的定价页面详细说明了这一点,它是根据你的索引大小和搜索次数的组合来估算的。目前,免费层提供每月10,000次搜索请求,每月10,000次搜索建议,以及在任何特定时间索引中的10,000个文档。
如果你对价格感到满意,下一步就是注册并创建你的账户。在确认了你的电子邮件地址之后,你将被放入Algolia仪表盘中。
第一页指导你创建一个索引,这是建立搜索界面的一个必要部分。你的账户可以拥有你所需要的许多不同的索引--一个网站绝对有可能需要多个,这取决于网站的规模和复杂性。作为开发者,通常给东西命名是我们工作中最难的部分之一,但请随意使用你的网站名称。在我的例子中,我使用了 "猫"。
命名索引之后,会提示你添加你的数据--你不需要立即这样做,但在你设置的时候,这绝对是有帮助的。如果你是从Lunr来的,这就是我们遇到的一个重要区别:在客户端解决方案中,你在每次使用搜索时都会生成一个索引,或者在构建预建索引时生成索引。在Algolia的基于服务的方法中,索引就像一个空的数据库。你将索引创建为一个空桶,然后可以在其中添加、编辑和删除数据。举个例子,如果你现在在Algolia中向你的索引添加了100个文档,那么这个索引就已经被播种了,并准备好进行搜索,而无需在客户端进行耗时的构建。你只需要更新索引以反映你的数据集的变化,但绝不需要重新加载你已经交给Algolia的数据。最初,就播种而言,Algolia将提供几个选项。
在这一点上,你如何对索引进行播种完全取决于你和你的数据的存储方式。我是一个Jamstack的开发者,所以我会生成一个可供搜索的可收养猫咪的JSON文件,运行我的网站的构建,并手工复制该JSON。
上传之后,我就被丢到一个界面,让我看到我的索引,最重要的是,立即开始测试搜索。
在这一点上,你有一个持久化的索引。它已经完成,并准备好进行搜索。如果你使用的是应用服务器,比如Node或PHP,你的数据可能在MySQL或MongoDB之类的地方,在这种情况下,你可以写一个自定义查询来生成你想要的数据,并通过Algolia API将其输入索引。你所使用的后端语言可能有一个SDK。
所有这些都是由REST API支持的,所以如果你不喜欢这些选项,你可以自己ping正确的端点。
让我们考虑一下我自己博客的真实例子。与Lunr或其他客户端方法一样,我确实生成了一个JSON文件,其中包括博文标题、发布日期、内容、相关类别和适当的标签。作为构建过程的一部分,我只更新最近的内容(因为那是唯一改变的内容)。首先,我导入Node SDK并初始化一个索引对象:
const algCredentials = { appId: process.env.ALG_APP_ID, apiKey: process.env.ALG_API_KEY, indexName: 'raymondcamden' };
const algoliaSearch = require('algoliasearch');
const algolia = algoliaSearch(algCredentials.appId, algCredentials.apiKey);
const index = algolia.initIndex(algCredentials.indexName);
然后我得到一份最近内容的JSON副本:
let dataResp = await fetch('https://www.raymondcamden.com/algolia_new.json');
let data = await dataResp.json();
然后我创建一个批处理请求来更新索引:
let requests = [];
// If you use my code for a new blog, your index may not have 3 items!
for (let i=0; i<3; i++) {
/* define an objectID for Algolia */
let d = data[i];
d.objectID = d.url;
requests.push({
'action': 'updateObject',
'body': d
})
};
console.log('Batch data object created to add to Algolia index');
let batchResult = await index.batch(requests);
我不必担心重新索引现有的内容,因为updateObject ,通过每条记录上唯一的objectID 值来处理新内容与现有内容的合并。Algolia的索引是永久性的,所以我不必担心任何事情,只需担心自上次更新索引后明确改变的内容。我的网站有6,000多篇博客文章,所以这对我来说是件大事。可以说,Algolia需要我做更多的工作,但这种可扩展性使它更值得。
添加搜索
在Lunr,设置搜索是一个100%的手工过程。我们需要自己获取用户的输入,自己调用库,使用返回的参考文献来从数据库中获取我们的结果,并且自己渲染所有这些。如果你需要可定制性,Algolia给了你这个选择:在服务器端使用的相同的JavaScript客户端也可以在客户端使用,以手动完成这些。该代码可能看起来像这样:
let client = algoliasearch('WFABFE7Z9Q', 'd1c88c3f98648a69f11cdd9d5a87de08');
let index = this.client.initIndex('raymondcamden');
// later, to actually search:
let resultsRaw = await this.index.search(search, {
attributesToRetrieve:['title', 'url', 'date'],
attributesToSnippet:['content'],
hitsPerPage: 50,
clickAnalytics:true
});
请注意,Algolia让你过滤返回的内容。这很好,因为它可以让你索引一大块文本并进行搜索,比如博客文章的正文,但在处理结果时只关心标题、网址和日期。我甚至可以指定返回博客内容,但只返回相关片段。
在我的网站上,这些数据就会在DOM上呈现出来。
这种手动方式让我完全控制了搜索体验,但是Algolia做了更多的努力,给了我们一个开箱即用的解决方案,叫做InstantSearch.js。这让你可以通过widget创建一个搜索界面,所有的重活都为你完成。库和组件可用于多个不同的前端框架和移动平台。
这个库极大地简化了前端的搜索过程,同时仍然给你提供你所需要的控制。
获得洞察力
迁移到Algolia后,最大的好处之一是能够看到你的搜索统计数据并采取行动。在仪表盘中,你会看到关于有多少搜索被执行,有多少人在搜索,以及他们在查询后做什么的冷酷数据。
这个概览还报告了最常见的搜索,最常见的结果,以及关键的是,没有返回任何东西的搜索。
你还可以(通过设置)启用每周一次的电子邮件,总结你的网站最近的搜索历史。它包括围绕使用情况的基本统计资料,以及搜索和没有结果的细节。
更进一步
在这篇文章中,我主要侧重于从Lunr迁移到Algolia,但我们并没有真正触及迁移之后可以做什么。Algolia为我们提供了强大的、由人工智能驱动的搜索功能--这是我们在Lunr中从未拥有的--所有这些都可以通过仪表盘进行配置,以便在整个组织中更容易使用。与任何工程决策一样,开发人员必须权衡功能与实施时间和成本,但对我来说,这绝对值得。也许你今天应该试一试,测试一下迁移是否值得你这样做?