# 使用 Typesense 为 Firebase 实现全文搜索

Firebase (opens new window) 是由 Google 支持的流行应用开发平台,被全球开发者广泛使用。它提供了多种工具来构建、发布和监控您的应用程序。然而,Firebase 本身并不提供原生的索引或搜索解决方案。

Firebase 文档 (opens new window) 提到了使用 Algolia、Elasticsearch 等服务来实现全文搜索,最近还提到了 Typesense。Algolia 是一个不错的解决方案,但它是专有服务,即使对于中等规模的应用也可能成本高昂。Elasticsearch 是一个多功能的搜索解决方案,但学习曲线陡峭,在生产环境中运行 Elasticsearch 需要相当多的 DevOps 知识,即使使用 Elastic Cloud 也是如此。

Typesense 简介 - Typesense 是一个开源 (opens new window)的容错模糊搜索引擎,具有简洁的 API 和文档,易于使用、运行和扩展。
可以将其视为 Algolia 的开源替代品,以及比 ElasticSearch 更易用、功能更全面的替代方案。
Typesense 速度极快且高度可配置,您可以根据需求定制搜索结果。了解更多 Typesense 特性请访问这里 (opens new window)

您可以选择自托管 Typesense 或使用托管 SaaS 服务 Typesense Cloud (opens new window)

更新

我们发布了 Firebase 扩展,只需点击几下即可安装到您的 Firebase 项目中,
自动将 Firestore 文档同步到 Typesense。

设置方法如下:
https://github.com/typesense/firestore-typesense-search (opens new window)

如果选择使用此扩展,安装完成后可直接跳转到下面的步骤 4

本教程将展示如何将 Typesense 与 Firebase 集成,为您的 Firebase 应用构建全文搜索体验。

我们假设您已经熟悉 Firebase、Firestore 及其工作原理。让我们从一个存储书籍标题和出版年份的示例应用开始:

// books/${bookID}
{
    id: string,
    title: string,
    publication_year: int32
}

# 第一步:运行 Typesense

运行 Typesense 最简单的方式是使用 Typesense 云服务 (opens new window),这是 Typesense 的托管版本:

  1. 访问 https://cloud.typesense.org (opens new window) 并使用 GitHub 登录
  2. 选择您需要的配置或保留默认设置,然后点击 "Launch"
  3. 您的集群将在约 5 分钟内完成部署并运行。然后点击 "Generate API Key"

在本教程的后续部分,我们将使用您生成的 hostname 和 API keys。

您也可以选择自托管方式,在本地或 GCP 服务器上运行 Typesense。 这里提供了在任何云服务器上安装 Typesense 的说明。

# 第二步:创建 Typesense 集合

要使用 Typesense,我们首先需要创建一个客户端。Typesense 支持多种 API 客户端,包括 Javascript、Python、Ruby、PHP 等。

初始化 Javascript 客户端时,需要使用你在第一步中生成的 Typesense 服务器 API 密钥:

import Typesense from 'typesense'

let client = new Typesense.Client({
  'nodes': [{
    'host': 'xxx.a1.typesense.net', // xxx 是你的 Typesense Cloud 集群的 ClusterID
    'port': '443',
    'protocol': 'https'
  }],
  'apiKey': '<ADMIN_API_KEY>',
  'connectionTimeoutSeconds': 2
})

接下来,我们将创建一个集合。集合是一组相关文档的集合,你可以将其视为关系型数据库中的表。集合需要一个模式(schema),用于定义文档的结构。

const myCollection = {
  'name': 'books',
  'fields': [
    {'name': 'id', 'type': 'string'},
    {'name': 'title', 'type': 'string' },
    {'name': 'publication_year', 'type': 'int32' },
  ],
  'default_sorting_field': 'publication_year'
}

client.collections().create(myCollection)

我们创建了一个名为 books 的集合,存储在 books 集合中的文档将包含三个字段:idtitlepublication_year

id 是一个特殊字段,Typesense 将其用作文档的标识符。如果没有 id 字段,Typesense 会自动为文档分配一个标识符。注意 id 不应包含空格或任何需要在 URL 中编码 (opens new window)的字符。

在本教程中,我们将使用 Firestore 文档的 ID 作为 id 值。

# 第三步:向 Typesense 写入数据

接下来,我们将编写函数来监听 Firestore 的变更事件,并将变更写入 Typesense。

# 新建文档

我们创建一个函数,用于在Typesense中每当有新文档创建时将其添加到搜索索引(即collection):

exports.onBookCreate = functions.firestore.document('/books/{bookID}')
  .onCreate((snapshot, context) => {
    // 获取文档ID作为id值
    id = context.params.bookID
    const { title, publication_year } = snapshot.data();
    document = {id, title, publication_year}

    // 将文档索引到books集合中  
    return client.collections('books').documents().create(document)
  })

# 文档更新

同样地,你也可以更新和删除文档:

exports.onBookUpdate = functions.firestore.document('books/{bookID}')
  .onUpdate( (change, context) => {
    // 获取变更后的值
    const { id, title, publication_year } = change.after.data();
    document = {id, title, publication_year}
    return client.collections('books').documents(id).update(document)
  });

# 文档删除

对于删除操作,你只需要文档的ID:

exports.onBookDelete = functions.firestore.document('books/{bookID}')
  .onDelete((snap, context) => {
    // 获取文档ID
    id = context.params.bookID
  
    return client.collections('books').documents(id).delete()
  });

你也可以根据条件批量删除文档,具体方法参见这里

# 步骤4:开始搜索!

当数据被索引后,你可以使用简单的搜索参数进行查询:

let search = {
	'q' : '<SEARCH_VALUE>',
	'query_by': 'title',
}

client.collections('<COLLECTION_NAME>')
  .documents()
  .search(search)
  .then(function (searchResults) {
    console.log(searchResults)
  })

Typesense是一个容错搜索引擎。因此,即使你在搜索查询中输入有拼写错误,仍然能获得最相关的结果。

# 构建搜索界面

现在你可以使用 instantsearch.js (opens new window) 来添加搜索栏,这是一个由 Algolia 开发的开源 UI 组件集合。

Typesense 提供了一个 instantsearch 适配器 (opens new window),你可以用它来创建基于 UI 的搜索界面,同时将查询请求发送到 Typesense。

使用以下命令安装 Instantsearch 适配器:

npm install typesense-instantsearch-adapter react-instantsearch-dom @babel/runtime

接下来,使用 react-instantsearch 创建一个搜索界面:

import { InstantSearch, SearchBox, Hits, Stats } from "react-instantsearch-dom"
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter"
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
  server: {
    apiKey: "xyz", // 请确保使用 Search API Key
    nodes: [
      {
        host: 'xxx.a1.typesense.net', // xxx 是你的 Typesense Cloud 集群的 ClusterID
        port: '443',
        protocol: 'https'
      },
    ],
  },
  // 以下参数会直接传递给 Typesense 的搜索 API 端点
  // 因此你可以传递搜索端点支持的任何参数
  // query_by 是必填项
  additionalSearchParameters: {
    query_by: "title,description,tags",
  },
})
const searchClient = typesenseInstantsearchAdapter.searchClient
export default function SearchInterface() {
  const Hit = ({ hit }) => (
    <p>
      {hit.title} - {hit.description}
    </p>
  )
return (
      <InstantSearch searchClient={searchClient} indexName="pages_v1">
        <SearchBox />
        <Stats />
        <Hits hitComponent={Hit} />
      </InstantSearch>
  )
}

Instantsearch.js 功能非常强大,你可以用它创建各种有趣的搜索组件。更多信息请参阅这里 (opens new window)

就是这样!如你所见,Typesense 易于设置且使用简单。你可以将其集成到应用中,构建快速、容错的搜索界面。如果在使用 Typesense 时遇到任何问题或有新功能需求,请访问我们的 GitHub 仓库 (opens new window) 提交 issue。

如果对本指南有任何改进建议,请点击下方的"Edit page"链接向我们提交 pull request。