こんにちは。ヤフーエンジニアのわたなべせいりょです。
ここではスマートフォン版Yahoo!検索の作り方 第4回:JS編 - 検索をより便利にの中でご紹介させていただいた機能の実際の実装について書きたいと思います。
まず、長押し入力について書いてみたいと思います。「j」で「JavaScript」がサジェストされるのは良いのですが、「JavaScript array」を調べたい時など、一度「JavaScript」の検索結果に飛ばないといけないのを億劫に感じていました。
そういったところから長押し入力を思いついたのですが、実装に当たり、長押しにより入力ボックスの文字が変わり、後で説明いたしますが一定時間間隔で入力ボックスをチェックしていることから指を抑えたままの状態でサジェストが書き変わり、その状態でいるとまたそこを長押ししたと判定してしまったり、指を離すとタップしたと判定されたりすることを防ぐのに少し苦労しました。
次にインクリメンタルサーチの説明をしたいと思います。インクリメンタルサーチの実装は割と単純で、入力ボックスの値が変更されれば対応するHTMLを入れ替えるという方法を取っています。
JSONでデータだけ受け取る方法もあったのですが、共通フレームワークであることから、それぞれのサービスのデザインをJSで持つことは避けたかったので、現在の仕様にしました。インクリメンタルサーチの場合、JavaScriptでDOMを変えていくことからページ遷移が発生しませんので、例えば何も入力されていな状態から「abc」と入力した後、検索結果がJavaScriptで描画され、その後その結果のページへ移動した後、「戻る」を行っても何も入力されていな状態に戻るという問題がありました。
そこで、3秒間入力がなかった場合、または結果をクリックした時にフラグメント識別子(#)へpパラメータを付加し、その後ブラウザ移動が起こったときのonhashchange イベントを利用して「戻る」「進む」の挙動を実現しました。
if (window.onhashchange != "undefined") {
$(window).bind('hashchange', function() {
// フラグメント識別子にpパラメータがあれば
if (hash=gethash("p")) {
// 入力ボックスにその値をセット
$('#SbIpt').val(decodeURI(hash));
}
});
}
// そのハッシュからパラメータの値を取得
function gethash(param) {
var hash = window.location.hash;
var re = new RegExp("([]" + param + "=)([^&]*)");
return hash.match(re) ? hash.match(re)[2] : "";
}
次は、検索履歴について触れてみます。
スマートフォンだしローカルストレージが使えるだろう、ということでローカルストレージを利用した検索履歴の保存を行い、同時に検索履歴からのサジェストも行えるようにしました。デリミタはタブを利用しています。Yahoo!検索ではクエリ内のタブはスペースへ変換されるため、デリミタとして適しています。制御文字でもよかったのですが、デバッグのしやすさからこちらを採用しました。
// タブの正規表現
var REGTAB = RegExp("\x09", 'g');
function set_hist() {
// ローカルストレージから履歴データ取り出し
var histarr = localStorage.getItem("search.sugguest.history").split(REGTAB);
// 検索クエリの取り出し ※実際はサーバから受け取ります
query = document.URL.match(/([&?]p=)([^&]*)/) ? decodeURI(decodeURI(document.URL
.match(/([&?]p=)([^&]*)/)[2]))
// 履歴データに追加
histarr.unshift(query);
// ローカルストレージにセット
localStorage.setItem("search.sugguest.history", histarr.join("\x09"));
}
スマートフォンのバージョンによってはローカルストレージが使えませんので、その場合はGearsを利用します。
<script src="gear5-0.3.js"></script>
ローカルストレージが使えないブラウザの時だけGears を読み込むようにすることで、容量の増大を抑えています。
次に、入力補助の実装について触れたいと思います。
一定間隔で入力ボックスをチェックしており、内容に変化があればAPIをたたき、サジェストを表示するようにしています。onkeyupをトリガーにしてもいいのですが、ブラウザ間のタイミングの違いや、素早く入力された場合を考慮し、現在の方法を取っています。
var inp_keep;
function inptimer() {
// 入力ボックスからワードを取得
var inp = $('#SbIpt').val();
// 入力に変化があった場合に処理を行う
if (inp_keep != inp) {
inp_keep = inp;
/*
* todo:検索履歴からのサジェスト処理
*/
// サジェストAPIをたたく
$.ajax({
url : api + "?p=" + inp,
dataType : "jsonp",
data : {},
success : function(json) {
/*
* サジェスト描画
*/
setTimeout(inptimer, 100);
},
error : function() {
setTimeout(inptimer, 100);
}
});
} else {
// 入力に変化がなければ一定間隔で再度チェック
setTimeout(inptimer, 100);
}
}
通常のサジェストに加え、検索履歴からも入力補助が出せるようにしています。
これにより、例えば通常のサジェストでは「e」で「映画」が出るのかもしれませんが、いつも「e」から「erlang 」を検索している人にとってはそちらが入力補助として出た方がユーザビリティとして高いと考えました。本当は検索履歴内でマッチするものが5つあれば5つとも出したいと考えていたのですが、社内での議論の結果1行だけしか出さないということになりました。
ログから検索履歴が良くつかわれているようであれば表示を増やすことも検討するということになっていますので、履歴からの入力補助をたくさん使っていただけると嬉しいです。コードとしては入力補助で利用したタイマーに入れ込む形で実装します。履歴は前方一致で、カタカナも含まれるようにしています。
/*
* 検索履歴からのサジェスト処理
*/
// 前方一致の正規表現
var reginp = RegExp("^" + inp, 'i');
// カタカナも含む
var reginpkk= RegExp("^" + inp.toKatakanaCase());
// 履歴データを検索
for ( var i = 0; i < histarr.length; i++) {
var hist = histarr[i];
// 入力ワードと前方一致するものがあれば履歴からサジェスト
if (hist.match(reginp) || hist.match(reginpkk)) {
/*
* 履歴からのサジェスト描画
*/
}
}
また、最初の文字と最後のクエリを関連付けることにより、アルファベット一文字でも履歴を呼び出せるようにもしてみました。例えば、前方一致であれば「ro」と打ち、「ロ」に変換しなければ「ローカルストレージ」と出てきませんが、これであれば「r」と打てばいつも検索している「ローカルストレージ」が表示されます。
こちらもタブをデミリタとして使用しています。
// タブ2つの正規表現
var REGTABTAB = RegExp("\x09\x09", 'g');
// サブミット前処理
function pre_submit(query) {
// ローカルストレージから履歴データ取り出し
fst_arr = localStorage.getItem("search.sugguest.history.first").split(REGTABTAB);
// 履歴データに最初に入力した1文字と最後のクエリを保存
fst_arr.unshift(query_first_str + "\x09" + query);
// ローカルストレージにセット
localStorage.setItem("search.sugguest.history.first", fst_arr.join("\x09\x09"));
}
いかがでしたでしょうか。
履歴に関してはその頻度のデータも取得し、よりパーソナライズされた入力補助を出せるようにするなどを考えています。
他にも工夫を続け、検索ブログでも書きましたがより早く目的の検索結果を表示できるよう、またユーザビリティへもこだわり、改善していきたいと思っていますので、今後ともよろしくお願いいたします。
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました