バックエンド

Firestoreネイティブ検索入門——全文検索・地理空間検索をNext.jsで使う

Firestore Enterpriseのネイティブ全文検索・地理空間検索を、Next.js App RouterとTypeScriptでどう使うかを実践形式で解説します。外部検索基盤を増やす前に確認したい設計ポイントをまとめます。

2026年6月19日
FirestoreNext.jsTypeScript検索Firebase
Firestoreネイティブ検索入門——全文検索・地理空間検索をNext.jsで使う

はじめに

Firestoreで検索機能を作るとき、こんな設計に悩んだことはありませんか?

  • 商品名や説明文を横断検索したいが、標準クエリだけでは表現しづらい
  • 店舗や案件を「現在地から近い順」に出したい
  • Algolia、Typesense、Meilisearchなどを足すほどの規模か判断しづらい
  • Firestoreと検索インデックスの同期ずれを運用で吸収している

2026年4月のFirebase / Google Cloud Next 2026発表で、Firestore Enterprise editionに ネイティブ全文検索地理空間検索 がPreviewとして追加されました。公式リリースノートでも、Firestore Enterprise edition in Native modeとPipeline operationsはGA、Text searchとGeospatial searchはPreviewと整理されています。

本記事では、架空の店舗管理SaaS「TenpoFlow」を題材に、Next.js App Router + Firestore + TypeScriptで検索機能を組む場合の実装イメージを解説します。

何が変わったのか

従来のFirestoreでは、前方一致や配列検索はできても、関連度を考慮した全文検索や、距離条件を使った地理空間検索は外部サービスに寄せることが多い構成でした。

今回のポイントは、検索がFirestore EnterpriseのPipeline operationsの一部として扱えるようになったことです。Firebase公式ブログでは、Firestore内にGoogleの検索技術を統合し、ライブデータベースと一貫した検索結果を返せることが説明されています。

使いどころを整理すると、次のようになります。

やりたいこと新機能での考え方注意点
キーワード検索documentMatches を使うText indexが必要
フレーズ検索検索文字列にフレーズを渡す対象はインデックス済みフィールド
近隣検索geoDistance を使うGeospatial indexが必要
距離条件lessThan / <= 相当で絞る距離単位はメートル
本番採用Enterprise editionで検証Text / GeospatialはPreview

事前準備

公式ドキュメント上、全文検索と地理空間検索には次の前提があります。

  • Firestore Enterprise edition databaseが必要
  • 検索対象フィールドには、事前にText indexまたはGeospatial indexを作る必要がある
  • Pipeline operationsを使うSDKの初期化が必要
  • 2026年6月19日時点で、Text searchとGeospatial searchはPreview機能

インデックス作成の具体手順は、プロジェクトや利用SDKにより変わります。存在を確認していないCLIコマンドを書くのは危険なので、ここでは公式の「Manage indexes」と各検索ドキュメントを参照する前提にします。

参考にした一次情報は次の通りです。

ハンズオン1: 店舗データを設計する

TenpoFlowでは、複数店舗をFirestoreの branches コレクションで管理します。検索対象は店舗名、説明、対応サービス、住所、位置情報です。

// src/types/branch.ts
import type { GeoPoint, Timestamp } from "firebase/firestore";

export type Branch = {
  id: string;
  name: string;
  description: string;
  services: string[];
  address: string;
  location: GeoPoint;
  isPublished: boolean;
  updatedAt: Timestamp;
};

検索体験としては、最初から全部を1つのクエリに押し込まないのが実務的です。まずは「キーワード検索」と「近くの店舗検索」を別々のユースケースとして作ります。

ハンズオン2: 全文検索をServer Actionから呼ぶ

FirestoreのWeb SDKでは、Pipeline operations用のAPIとして firebase/firestore/pipelines が提供されています。公式サンプルでは、execute のimportにより db.pipeline() が使えるようになる点も示されています。

// src/lib/firebase/enterpriseDb.ts
import { getFirestore } from "firebase/firestore";
import { app } from "@/lib/firebase/client";

export const enterpriseDb = getFirestore(app, "enterprise");

次に、検索用の関数を作ります。ユーザー入力をそのまま広げず、空文字と長すぎる入力を先に弾くのがポイントです。

// src/lib/branches/searchBranchesByText.ts
import {
  documentMatches,
  execute,
  score,
} from "firebase/firestore/pipelines";
import { enterpriseDb } from "@/lib/firebase/enterpriseDb";

export async function searchBranchesByText(keyword: string) {
  const queryText = keyword.trim().slice(0, 80);

  if (!queryText) {
    return [];
  }

  const snapshot = await execute(
    enterpriseDb
      .pipeline()
      .collection("branches")
      .search({
        query: documentMatches(queryText),
        sort: score().descending(),
      })
      .limit(20),
  );

  return snapshot.results.map((result) => ({
    id: result.id,
    ...result.data(),
  }));
}

Server Action側では、フォームから受け取った値をこの関数に渡します。認証や公開状態の制御はアプリ側の設計に合わせますが、少なくとも「検索フォームから任意の巨大文字列を渡せる」状態は避けます。

// src/app/branches/actions.ts
"use server";

import { searchBranchesByText } from "@/lib/branches/searchBranchesByText";

export async function searchBranchesAction(formData: FormData) {
  const keyword = String(formData.get("keyword") ?? "");
  return searchBranchesByText(keyword);
}

UIは通常のServer Componentから組み立てられます。

// src/app/branches/page.tsx
export default function BranchSearchPage() {
  return (
    <form action={searchBranchesAction} className="space-y-3">
      <input
        name="keyword"
        className="w-full rounded border px-3 py-2"
        placeholder="例: 個室 会議室 新宿"
      />
      <button className="rounded bg-black px-4 py-2 text-white">
        検索する
      </button>
    </form>
  );
}

ハンズオン3: 現在地から近い店舗を探す

地理空間検索では、公式ドキュメントにある通り geoDistance を使います。距離はメートルで指定し、条件は「指定距離以下」の形になります。

// src/lib/branches/searchNearbyBranches.ts
import { GeoPoint } from "firebase/firestore";
import { execute, field } from "firebase/firestore/pipelines";
import { enterpriseDb } from "@/lib/firebase/enterpriseDb";

type NearbyInput = {
  latitude: number;
  longitude: number;
  radiusMeters: number;
};

export async function searchNearbyBranches(input: NearbyInput) {
  const radiusMeters = Math.min(Math.max(input.radiusMeters, 100), 5000);
  const center = new GeoPoint(input.latitude, input.longitude);

  const snapshot = await execute(
    enterpriseDb
      .pipeline()
      .collection("branches")
      .search({
        query: field("location").geoDistance(center).lessThan(radiusMeters),
      })
      .limit(20),
  );

  return snapshot.results.map((result) => ({
    id: result.id,
    ...result.data(),
  }));
}

半径はサーバー側でも必ず丸めます。クライアントのselectで「500m / 1km / 3km」を用意していても、開発者ツールから任意値を送れるためです。

GitHub Actionsで最低限の破損を防ぐ

Preview機能を使うときほど、CIでは「少なくともビルドが壊れていない」ことを機械的に確認します。Firestore Enterpriseの実DBをCIから叩くかは権限設計次第ですが、型エラーとNext.jsビルドは毎回確認したいところです。

# .github/workflows/web-ci.yml
name: Web CI

on:
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: yarn
      - run: yarn install --frozen-lockfile
      - run: yarn build

検索クエリそのものの結合テストは、SDK・エミュレータ・Enterprise databaseの対応状況を公式ドキュメントで確認してから追加します。未確認のエミュレータ起動オプションをCIに書くより、最初は入力バリデーションやUIのE2Eから固める方が安全です。

採用判断のポイント

Firestoreネイティブ検索は魅力的ですが、外部検索サービスを完全に置き換える前提で急ぐ必要はありません。

観点Firestoreネイティブ検索が合うケース外部検索基盤も検討するケース
データ同期Firestore内で完結させたい複数DBやCMSを横断したい
検索要件キーワード、フレーズ、近隣検索が中心同義語、ランキング調整、分析UIが重要
運用構成要素を減らしたい検索専任の運用・改善がある
安定性Previewを検証環境から試せるGA機能だけで本番を組みたい

モリソンのようにNext.js + Firestoreで業務アプリを作る場合、まずは「検索同期用のCloud Functionsを減らせるか」「外部検索サービスのインデックス遅延をなくせるか」を検証すると効果が見えやすいです。

まとめ

Firestore Enterpriseの全文検索・地理空間検索により、Firestoreだけで扱える検索体験の幅が広がりました。

  • Text searchとGeospatial searchはFirestore Enterprise editionで利用するPreview機能
  • どちらもPipeline operationsの search(...) stageとして扱う
  • 全文検索は documentMatches、地理空間検索は geoDistance を使う
  • 検索対象フィールドには対応するインデックスが必要
  • Next.jsではServer ActionやRoute Handlerの裏側に検索関数を閉じ込めると扱いやすい

まずは検証用のEnterprise databaseで、検索対象を1コレクションに絞って試すのがおすすめです。外部検索基盤を増やす前に、Firestore内で完結できる範囲を見極めるだけでも、設計判断がかなりクリアになります。