Azure Searchのハイライト機能 & ハイライト機能の癖を回避した実装について
はじめに
Azure Searchの検索結果はデフォルトだと、キーワードにヒットした本文をハイライトしてくれません
なのでAzure Searchの検索結果のハイライトを実装したいと思います。
ハイライト機能はやや癖があるので、癖を回避した実装について書きます
<ハイライトを使用しない場合の検索結果イメージ>
基礎部分の作成
npm install
npm install npm express ejs request --save
index.js ※Azure Searchの設定関連は未入力状態になっています
// /////////////////////////////////////////////////////////////////////////////////////// Azure Searchの設定関連 // /////////////////////////////////////////////////////////////////////////////////////// Azure Searchのサービス名constsearchServiceName='';// Azure SearchのクエリキーconstqueryKey='';// Azure Searchのインデクス名constindexName='';// コンテンツを保持しているフィールド名(ほとんどの場合はcontent、OCRのマージフィールドを指定する場合はmerged_content) constcontent_field_name="content";// /////////////////////////////////////////////////////////////////////////////////////// 定義関連 // /////////////////////////////////////////////////////////////////////////////////////// MVCフレームワークとしてexpressを利用するための設定varexpress=require('express');varapp=express();// ejsをビューに使う為の設定app.set('view engine','ejs');// 非同期処理における例外発生時にエラーに繋ぐためのラッパーconstasyncwrap=fn=>(req,res,next)=>fn(req,res,next).catch(next);// 静的コンテンツを外部ファイル化(publicフォルダ配下を<ROOT>/staticでアクセス許可)app.use('/static',express.static('public'));// /////////////////////////////////////////////////////////////////////////////////////// 検索の初期表示 と 検索実施// http://localhost:8080/にアクセスしたときの処理// /////////////////////////////////////////////////////////////////////////////////////app.get('/',asyncwrap(async(req,res)=>{// 画面から投げた検索キーワードの設定。 キーワードが投げられていない場合はワイルドカード(*=条件未指定)を設定するconstq=req.query.keyword||'*';console.log(q);// キーワードをエンコードして設定constquery=encodeURIComponent(q)+'&count=true&searchMode=all';// 検索実行varsearchResult=awaitnewPromise((resolve,reject)=>{constrequest=require('request');request({method:'GET',url:`https://${searchServiceName}.search.windows.net/indexes/${indexName}/docs?api-version=2019-05-06&search=${query}`,headers:{'Content-type':'application/json','api-key':queryKey},json:true,},function(err,res,body){if(err){reject(err);}else{resolve(body);}});});// 通常の検索結果とハイライト付の検索結果はそれぞれ異なるフィールドに設定されるので、ハイライトを優先的に取得しますvarresult=[];for(vari=0;i<searchResult.value.length;i++){// 1.タイトル(ファイル名)の取得vartitle=searchResult.value[i].metadata_storage_name;// 2.本文の取得varbody=searchResult.value[i][`${content_field_name}`];result.push({'title':title,'body':body});}// index.ejsに検索結果を渡して画面描画res.render('index',{searchResult:result,inputKeyword:q});}));// /////////////////////////////////////////////////////////////////////////////////////// 起動// /////////////////////////////////////////////////////////////////////////////////////app.listen(8080,()=>console.log('access -> http://localhost:8080/'))
index.ejs ※viewsフォルダ配下に入れましょう
<!DOCTYPE html><html><head><metacharset="utf-8"/><linkrel="stylesheet"media="all"href="./static/style.css"/><title>ハイライト</title></head><body><%//************************************************%><%//検索条件を設定し、検索を行う為のフォームエリア%><%//***********************************************%><formstyle="position:relative; margin-bottom:20px;"action="/"><%//キーワード入力%><inputid="keyword"class="keyword"name="keyword"type="text"placeholder="キーワードを入力"value="<%= inputKeyword %>"/><%//検索ボタン%><inputtype="submit"class="submitsearch"value="検索"/></form><%//************************************************%><%//検索結果表示%><%//***********************************************%><%for(vari=0;i<searchResult.length;i++){%><%//ファイル名%><pclass="filename"><%=searchResult[i].title%></p><%//本文%><pid="a<%= i %>"class="docmain"><%-searchResult[i].body%></p><%//隙間調整%><br><%}%></body></html>
style.css ※publicフォルダ配下に入れましょう
.docmain{width:850px;margin:0000;padding:12px15px;color:#777;background:#fafafa;border:1pxsolid#ddd;position:relative;left:40px;}.keyword{outline:0;height:50px;padding:010px;left:0;top:0;width:230px;border-radius:2px;background:#eee;}.submitsearch{width:70px;height:50px;left:260px;top:0;border-radius:2px;background:#7fbfff;color:#fff;font-weight:bold;font-size:16px;border:none;}
ハイライトの実装
ハイライトの実装に必要な要素は大きく2つです。
1つ目がハイライト検索を行う為のクエリの作成です
ハイライトの要求はクエリで行う為、クエリに以下の3つのパラメータを追加します。
パラメータ | 説明 | 例 |
---|---|---|
highlight | どのフィールドをハイライトしたいか | 本記事ではcontentフィールド等を指定 |
highlightPreTag | ハイライト開始に指定したいタグ | 本記事ではmarkタグを指定 |
highlightPostTag | ハイライトの終了に指定したタグ | 本記事では/markタグを指定 |
※markタグは囲った文字列をマーカー調にハイライトしてくれます
クエリ周りを以下のように実装します
// Azure Searchのハイライトは以下のようにクエリで指定します。// ハイライトの設定(検索結果に含まれるキーワードを<mark>タグで囲うように設定)varhighlight=`&highlight=${content_field_name}-3&highlightPreTag=<mark>&highlightPostTag=</mark>`;// キーワードをエンコードして設定constquery=encodeURIComponent(q)+'&count=true&searchMode=all'+highlight;
2つ目がハイライトされた検索結果の取得です
ハイライトされた文字列は、本文とは異なるフィールドにマップされる為
以下のような実装が必要になります。
ハイライトされている場合 → ハイライトフィールドをサマリとして活用
ハイライトされていない場合 → デフォルトフィールドをサマリとして活用
// 2.本文の取得varbody=null;if(searchResult.value[i]['@search.highlights']!=undefined){// ハイライトが存在する場合lethighlights=searchResult.value[i]['@search.highlights'];body=highlights[`${content_field_name}`].join('\n');}else{// ハイライトが存在しない場合body=searchResult.value[i][`${content_field_name}`];}
ハイライトの動作確認をします
キーワードにマッチする本文がハイライトされていることがわかります
しかしハイライトされているのは一部で本文全体がハイライトされているわけではありません。
ハイライト処理のカスタマイズ
本文全体の中からマッチするワードをハイライトするようにカスタマイズします
一番オーソドックスなやりかたとしては
①『ハイライト文章』からハイライトタグを除去して、『未ハイライト文章』を作成
② 本文から『未ハイライト文章』を検索して、『ハイライト文章』で置換します
①②を繰り返すことで、本文全体の中からマッチするワードがハイライトされるようになります。
// 2.本文の取得varbody=null;if(searchResult.value[i]['@search.highlights']!=undefined){// 本文body=searchResult.value[i][`${content_field_name}`];// ハイライトが存在する場合lethighlights=searchResult.value[i]['@search.highlights'];for(varj=0;j<highlights[`${content_field_name}`].length;j++){// ハイライトを1つずつ取得 lethighlight=highlights[`${content_field_name}`][j];// ハイライトタグを除去して、未ハイライト文章を作成するletnotHighlight=highlight.replace(/<mark>/g,'').replace(/<\/mark>/g,'');// 本文から未ハイライト文章を検索し、ハイライト済文章で置換するbody=body.replace(notHighlight,highlight);}}
カスタマイズしたハイライトの動作確認をします
本文全体がハイライトされていることが確認できました
↑ 構造が複雑なPDFファイル等をこの手法でハイライトする場合は
構造データが本文フィールドに混じる場合があり、ハイライトフィールドには混じらないことがあるので
そういった場合は、もう1工夫が必要です。(上記手法だけだと、置換の為の検索対象が本文に存在しないので、置換がうまくいかない場合があります。)
※現時点ではそうなってしまう状態ですが、バージョンアップでいずれ解消するかもしれません。
最終的なindex.jsのソースです
// /////////////////////////////////////////////////////////////////////////////////////// Azure Searchの設定関連 // /////////////////////////////////////////////////////////////////////////////////////// Azure Searchのサービス名constsearchServiceName='';// Azure SearchのクエリキーconstqueryKey='';// Azure Searchのインデクス名constindexName='';// コンテンツを保持しているフィールド名(ほとんどの場合はcontent、OCRのマージフィールドを指定する場合はmerged_content) constcontent_field_name="content";// /////////////////////////////////////////////////////////////////////////////////////// 定義関連 // /////////////////////////////////////////////////////////////////////////////////////// MVCフレームワークとしてexpressを利用するための設定varexpress=require('express');varapp=express();// ejsをビューに使う為の設定app.set('view engine','ejs');// 非同期処理における例外発生時にエラーに繋ぐためのラッパーconstasyncwrap=fn=>(req,res,next)=>fn(req,res,next).catch(next);// 静的コンテンツを外部ファイル化(publicフォルダ配下を<ROOT>/staticでアクセス許可)app.use('/static',express.static('public'));// /////////////////////////////////////////////////////////////////////////////////////// 検索の初期表示 と 検索実施// http://localhost:8080/にアクセスしたときの処理// /////////////////////////////////////////////////////////////////////////////////////app.get('/',asyncwrap(async(req,res)=>{// 画面から投げた検索キーワードの設定。 キーワードが投げられていない場合はワイルドカード(*=条件未指定)を設定するconstq=req.query.keyword||'*';console.log(q);// Azure Searchのハイライトは以下のようにクエリで指定します。// ハイライトの設定(検索結果に含まれるキーワードを<mark>タグで囲うように設定)varhighlight=`&highlight=${content_field_name}-3&highlightPreTag=<mark>&highlightPostTag=</mark>`;// キーワードをエンコードして設定constquery=encodeURIComponent(q)+'&count=true&searchMode=all'+highlight;// 検索実行varsearchResult=awaitnewPromise((resolve,reject)=>{constrequest=require('request');request({method:'GET',url:`https://${searchServiceName}.search.windows.net/indexes/${indexName}/docs?api-version=2019-05-06&search=${query}`,headers:{'Content-type':'application/json','api-key':queryKey},json:true,},function(err,res,body){if(err){reject(err);}else{resolve(body);}});});// 通常の検索結果とハイライト付の検索結果はそれぞれ異なるフィールドに設定されるので、ハイライトを優先的に取得しますvarresult=[];for(vari=0;i<searchResult.value.length;i++){// 1.タイトル(ファイル名)の取得vartitle=searchResult.value[i].metadata_storage_name;// 2.本文の取得varbody=null;if(searchResult.value[i]['@search.highlights']!=undefined){// 本文body=searchResult.value[i][`${content_field_name}`];// ハイライトが存在する場合lethighlights=searchResult.value[i]['@search.highlights'];for(varj=0;j<highlights[`${content_field_name}`].length;j++){// ハイライトを1つずつ取得 lethighlight=highlights[`${content_field_name}`][j];// ハイライトタグを除去して、未ハイライト文章を作成するletnotHighlight=highlight.replace(/<mark>/g,'').replace(/<\/mark>/g,'');// 本文から未ハイライト文章を検索し、ハイライト済文章で置換するbody=body.replace(notHighlight,highlight);}}result.push({'title':title,'body':body});}// index.ejsに検索結果を渡して画面描画res.render('index',{searchResult:result,inputKeyword:q});}));// /////////////////////////////////////////////////////////////////////////////////////// 起動// /////////////////////////////////////////////////////////////////////////////////////app.listen(8080,()=>console.log('access -> http://localhost:8080/'))