⛩️

AI参拝ナビを作る #1:Django REST Frameworkで検索APIを実装する

に公開

はじめに

「AI参拝ナビ」という神社アプリを開発しています。
MVP(最小実用プロダクト)として、まずは「神社を検索できるAPI」をDjango REST Frameworkで実装しました。

  • バックエンド: Django + DRF + PostgreSQL (Docker)
  • フロントエンド: Next.js + Expo
  • 地図: Google Maps API / Mapbox
  • AI: OpenAI Responses API

今回は 検索API 実装の工程を紹介します。


モデル定義

神社 (Shrine) とご利益タグ (GoriyakuTag) をManyToManyで関連付けています。

class GoriyakuTag(models.Model):
    name = models.CharField(max_length=50, unique=True)

class Shrine(models.Model):
    name_jp = models.CharField(max_length=100)
    name_romaji = models.CharField(max_length=100, blank=True, null=True)
    address = models.CharField(max_length=255)
    latitude = models.FloatField()
    longitude = models.FloatField()
    goriyaku_tags = models.ManyToManyField(GoriyakuTag, related_name="shrines")

ご利益タグはマスターデータとして 15種類をfixturesから投入
例: 「縁結び」「金運・商売繁盛」「健康長寿」など。

APIエンドポイント
/api/goriyaku-tags/

ご利益タグ一覧を返す。

[{"id":1,"name":"縁結び"},{"id":5,"name":"金運・商売繁盛"}]

/api/shrines/

神社一覧(ご利益タグも含む)。
さらにクエリパラメータで検索に対応。

タグ検索: ?tag=縁結び

名前検索: ?name=明治

現在地検索: ?lat=35.66&lng=139.70&radius=5000

半径検索の実装

PostGISは使わず、まずはシンプルに ハーサイン式 (haversine formula) で距離を計算しました。

from math import radians, cos, sin, asin, sqrt

def haversine(lon1, lat1, lon2, lat2):
    lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
    dlon, dlat = lon2 - lon1, lat2 - lat1
    a = sin(dlat/2)**2 + cos(lat1)*cos(lat2)*sin(dlon/2)**2
    return 6371000 * 2 * asin(sqrt(a))  # 地球半径6371km


ViewSetで利用:

if lat and lng:
    ids = []
    for shrine in queryset:
        distance = haversine(lng, lat, shrine.longitude, shrine.latitude)
        if distance <= radius:
            ids.append(shrine.id)
    queryset = queryset.filter(id__in=ids)

ハマりどころ

curlで日本語検索が通らない → ?tag=縁結び はエンコード必須

ShrineViewSetが2重定義 → 後ろで上書きされてフィルタ無効になっていた

querysetをリスト化するとfilterできない → filter(id__in=ids) に修正

ブランチ運用

feature/search-api → タグ・名前検索を実装

feature/location-search → 半径検索を追加

PR単位で進めることで安全に開発を進められました。

実行例

渋谷(lat=35.66, lng=139.70, 半径5km)で検索:

curl "http://localhost:8000/api/shrines/?lat=35.66&lng=139.70&radius=5000"


結果:

[
  {
    "id": 1,
    "name_jp": "明治神宮",
    "goriyaku_tags": [{"id":1,"name":"縁結び"}]
  }
]


京都の伏見稲荷大社は返ってこず、正しくフィルタされました ✅

次のステップ

フロントエンド(Next.js)でAPIを呼び出し、地図にマーカー表示

コンシェルジュ機能(干支/四柱推命ベースの提案)

ルート検索(Mapbox Directions API連携)

おわりに

MVP開発では「動くものを小さく作り、PR単位で積み重ねる」ことを意識しました。
次回は フロントエンドでの検索UI実装 を記事にまとめます!


---
GitHubで編集を提案

Discussion