静的サイトジェネレーターで作ったサイトの検索 API
長瀬 敦史
以前、このブログで 直接 GitHub API v3 を使って検索画面を作った方法 を紹介しましたが、業務でも同じ方法を試み、後述の理由で、自前で簡易検索 API を作りました。
kaizenplatform/doc-search-api on GitHub
仕組み
あくまでも GitHub API の代替として作ったので、簡易な作りになっています。
エンドポイント
GET /?lang=en&q=...
ページを検索します。パラメータ q
に検索語句を、lang
にロケールを指定します。
$ curl 'https://my-doc-search.herokuapp.com/?lang=en&q=javascript'
[{"title": "My JavaScript Tips 1", "url": "/tips/javascript/1"}]
POST /rebuild
Redis への取り込みをトリガーします。
timestamp
に現在時刻のエポックミリ秒、 token
に timestamp
と TOKEN_SECRET
で設定した文字列を結合したものを SHA-1 ダイジェストの hex 値を指定します。
$ curl -XPOST 'https://my-doc-search.herokuapp.com/rebuild' \
-d 'timestamp=1425731850078&token=bf222511ce3ef7775658a3ff923f4ddb25fe0d12'
{"success": true}
弊社では CI ビルドプロセスで Middleman でサイトをデプロイ後、以下の Rake タスクでリクエストを送っています。
desc 'Request rebuilding search index'
task :rebuild_sitemap => [:env] do
if api_base = ENV['DOC_SEARCH_API_BASE']
require 'digest/sha1'
require 'json'
secret = ENV['REBUILD_TOKEN_SECRET']
ts = (Time.now.to_f * 1000).to_i.to_s
token = Digest::SHA1.hexdigest ts + secret
res = %x{curl -XPOST #{api_base}/rebuild -d 'token=#{token}×tamp=#{ts}'}
json = JSON.parse res
raise json['message'] if json['message']
end
end
導入方法
README の Heroku ボタンから、Heroku ですぐ動く様になっています。
NODE_ENV
はステージ名。ロケール名と共に、Redis の保存キーに使われます: "doc:${NODE_ENV}:${LANG}"
1. sitemap.json
を用意する
以下の様な JSON ファイルが書き出せれば、ジェネレータは問いません。
ローカライズされている前提でできているので、一番上のキーはロケールで、その下に title
, description
, body
を持ったハッシュの配列が入っています。
{
"en": [
{
"title": "Page 1",
"description": "This is a sample page.",
"body": "Lorem ipsum dolor sit amet, consectetur adipisicing elit"
}
],
"ja": []
}
このファイルがデプロイされる URL を環境変数の SITEMAP_URL
に設定します。
弊社では、Middleman と middleman-i18n を使って、ローカライズしています。
# sitemap.json.jbuilder
%i{en ja}.each do|lang|
json.set! lang, sitemap.resources.select{|res|
res.metadata[:options][:lang] == lang && res.ext == '.html'
}
end
2. TOKEN_SECRET
を設定する
前途の POST /rebuild
エンドポイントに送信する token
を生成するための秘密の文字列です。
検索ページ例
slim で書いています。
---
layout: full
---
- content_for :title do
= "#{t 'search.title'}: "
.search-result data-api-base=ENV['DOC_SEARCH_API_BASE']
h1
= "#{t 'search.title'}: "
q.search-tearm
ul.entries
p.no-result style='display:none' = t 'search.no_result'
coffee:
unless m = document.location.search?.match /[\?&]q=([^&]+)/
document.location.assign $('.brand a').attr 'href'
return
q = decodeURIComponent m[1]
lang = $('html').attr 'lang'
link = $('a[rel=alternate]')
link.attr 'href', link.attr('href') + '?q=' + m[1]
$('q.search-tearm').text q
$('input.search-term').val q
$.ajax
url: $('.search-result').data('apiBase')
data: { lang, q }
xhrFields:
withCredentials: yes
crossDomain: yes
success: (res) ->
if res.length > 0
for {url, title} in res
$("""<li><a href="#{url}">#{title}</a></li>""").appendTo '.entries'
else
$('.entries').parent().find('.no-result').show()
GitHub API v3 を直接使わなかった理由
1. 検索語は、単語一致であり、部分一致ではない
JavaScript
という語句を見つけるために、java
というキーワードは使えません。
Search Code API でサポートの有無を問い合わせたところ、将来、サポートするかもしれないが、確約はできない、とのことです。
2. プライベートリポジトリに対応していない
そもそも access_token
無しのリクエストでプライベートリポジトリを検索することはできません。
$ curl 'https://api.github.com/search/code?q=in:file%20java%20repo:kaizenplatform/super-secret-project&callback=foo'
/**/foo({
"meta": {
"X-RateLimit-Limit": "10",
"X-RateLimit-Remaining": "7",
"X-RateLimit-Reset": "1425732905",
"X-GitHub-Media-Type": "github.v3",
"status": 422
},
"data": {
"message": "Validation Failed",
"errors": [
{
"message": "The listed users and repositories cannot be searched either because the resources do not exist or you do not have permission to view them.",
"resource": "Search",
"field": "q",
"code": "invalid"
}
],
"documentation_url": "https://developer.github.com/v3/search/"
}
})