« ScrapBook及びSave Page WEで保存したWebページをApache Solrで検索できるようにしてみた(その1) | トップページ | Dockerを使ったJupyter Labの環境を用意する »

2020.05.16

ScrapBook及びSave Page WEで保存したWebページをApache Solrで検索できるようにしてみた(その2)

Apache SolrにWebページのテキストデータを登録するところまでを
ScrapBook及びSave Page WEで保存したWebページをApache Solrで検索できるようにしてみた(その1)
に書きました。今回は、WebブラウザをクライアントにしてApache Solrからテキスト検索する簡単なサンプルコードを書いてみます。

Apache Solrが起動しているとき、標準的には、http://localhost:8983/solr/scrapbook/select?q=pythonのような形でテキスト検索を実行できます。するとJSON形式のレスポンスが返ってきます。これを一歩進めてWebブラウザをクライアントにした簡単なテキスト検索システムを作ります。

index.html


<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>ScrapBook Solr Search</title>
</head>
<body>
<h1>ScrapBook Solr Search</h1>
<form id="textbox">
<table border="0">
<tr><td>Query: </td><td><input type="text" id="query" size="80"/></td></tr>
<tr><td>Sub Queries: </td><td><input type="text" id="subqueries" size="80"/></td></tr>
<!- ->
<tr><td></td><td><input type="button" value="Search" id="search">
<input type="button" value="Clear" id="clear">
<a href="http://[host name]/[path to]/scrapbook_top.html" target="_blank">索引</a></td></tr>
</table>
</form>
<!- ->
<hr />
<p/><pre>
<span id="info"></span>
<span id="pages1"></span>
<span id="result"></span>
<span id="pages2"></span>
</pre>
<!- ->
<script type="text/javascript" src="solrsearch.js"></script>
<!- ->
</body>
</html>
</pre>
<pre class="brush: html"><!- -><br />
<script type="text/javascript" src="solrsearch.js"></script>>
<!- ->

sorsearch.js


// Soler Search Tool
// Variables
const askSolrURL = 'api.cgi?';
const askBibURL = 'bib.cgi?';
const beforeURL = '[path to ScrapBook]';
const afterURL = 'http://[host name]/~[user name]/ScrapBook';

const rowsMAX = 30;
const defOpt = '&fl=' + encodeURIComponent('id,title') +'&rows=' + rowsMAX;

// Subroutines
const waitLoad = new Promise((resolve, reject) => {
document.addEventListener('DOMContentLoaded',
() => {resolve();}, false);
});

const clickSearch = new Promise((resolve, reject) => {
document.getElementById('search').addEventListener('click', () => {
let start = 0
resolve(communicateWithServer(start));
}, false);
});

const enterText = new Promise((resolve, reject) => {
document.getElementById('query').onkeypress = (e) => {
const key = e.keyCode || e.charCode || 0;
if (key == 13) {
e.preventDefault();
}
};
document.getElementById('subqueries').onkeypress = (e) => {
const key = e.keyCode || e.charCode || 0;
if (key == 13) {
e.preventDefault();
}
};
});

const communicateWithServer = (start) => {
const xhr = new XMLHttpRequest();

let opt = defOpt + '&start=' + start;
let subqueries = document.getElementById('subqueries').value.split(',');
for (let x = 0; x < subqueries.length; x++) {
if (subqueries[x].length > 0) {
opt += '&fq=' + encodeURIComponent(subqueries[x]);
}
}
let query = document.getElementById('query');
let result = document.getElementById('result');
let arg = askSolrURL + 'q=' + encodeURIComponent(query.value) + opt;

xhr.open('GET', arg, true);
xhr.send(null);

xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let data = JSON.parse(xhr.responseText);
if (data === null) {
result.textContent = 'データが存在しませんでした。';
} else {
makeList(data);
document.getElementById('pages1').innerHTML =
makeTOC(data, start);
document.getElementById('pages2').innerHTML =
makeTOC(data, start);
}
} else {
result.textContent = 'サーバエラーが発生しました。';
}
} else {
result.textContext = '通信中...';
}
};
return xhr;
};

const makeList = (data) => {
let lines = data.response.docs;
document.getElementById('info').textContent = data.response.numFound +
' Documents Found';
if (lines.length === 0) {
document.getElementById('result').textContent = '';
}
let html = new Array(rowsMAX);
let sum = 0;
for (let i = 0; i < lines.length; i++) {
Promise.all([askBib(lines[i].id)]).then((o) => {
let bib = o[0];
let url = lines[i].id;
if (bib.response.message === 'OK') {
url = url.replace(beforeURL, afterURL);
let catURL = bib.response.record[5];
catURL = catURL.replace(beforeURL, afterURL);
html[i] = '[' + bib.response.record[0] + '] ' +
'<a href=\" target=\"_blank\">' +<br />
bib.response.record[2] + '</a> <a href=\"' +
bib.response.record[3] +
'\" target=\"_blank\">ソースURLを開く</a>' +<br />
' <a href=\"' + catURL + '\" target=\"_blank\">'
'カテゴリ:' + bib.response.record[4] + '</a>\n';
} else if (bib.response.message === 'ERROR') {
html[i] = url + ': 文書データを取得できませんでした。\n';
}
sum += 1;
if (sum === lines.length) {
document.getElementById('result').innerHTML = html.join('');
}
});
}
};

const askBib = (id) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();

let arg = askBibURL + id;

xhr.open('GET', arg, true);
xhr.send(null);

let data;
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
// console.log(xhr.responseText);
data = JSON.parse(xhr.responseText);
if (data === null) {
document.getElementById('result').textContent =
'インデックス情報が存在しませんでした。';
} else {
resolve(data);
}
}
};
});
};

const makeTOC = (data, start) => {
let html = '';
let iMax = data.response.numFound;
let maxPage = Math.ceil(iMax / rowsMAX);
let pos = Math.ceil(start / rowsMAX);
for (let x = 0; x < maxPage; x++) {
if (x !== pos) {
html += '<a href=\"javascript:onLinkClick(' +
x + ')\">' + x + '</a> ';
} else {
html += x + ' ';
}
}
return html;
};

const onLinkClick = (x) => {
communicateWithServer(x * rowsMAX);
}

const clickClear = new Promise((resolve, reject) =&t; {
document.getElementById('clear').addEventListener('click', () => {
document.getElementById('query').value = '';
document.getElementById('subqueries').value = '';
document.getElementById('info').textContent = '';
document.getElementById('pages1').textContent = '';
document.getElementById('pages2').textContent = '';
document.getElementById('result').textContent = '';
resolve();
}, false);
});

// Main Routine
waitLoad
.then(clickSearch)
.catch((e) => console.log(e));

waitLoad
.then(enterText)
.catch((e) => console.log(e));

waitLoad
.then(clickClear)
.catch((e) => console.log(e));

// EOF

api.cgi


#!/bin/sh

URL='localhost:8983/solr/scrapbook/select?'
cat <<HTTP_RESPONSE
Content-Type: application/json;charset=UTF-8


$(curl -s ${URL}${QUERY_STRING})
HTTP_RESPONSE
exit 0

# EOF

bib.cgi


#!/bin/sh

LISTFILE='./listfile.txt'
ID=$(echo ${QUERY_STRING} | awk -f decodeURL.awk)

RES=$(cat ${LISTFILE} | awk -F '\t' -v ID="${ID}" 'BEGIN{OFS="\t"}{if($2==ID)print $0}' | tr '\t' ',' | sed 's/,/","/g')

if [ ${#RES} -eq 0 ]; then
cat <<HTTP_RESPONSE0
Content-Type: application/json;charset=UTF-8


{"response":{"message":"ERROR", "id":"${ID}"}}
HTTP_RESPONSE0
exit 0
fi

cat <<HTTP_RESPONSE1
Content-Type: application/json;charset=UTF-8


{"response":{"message":"OK", "record":["${RES}"]}}
HTTP_RESPONSE1
exit 0

# EOF

solrsearch.jsはPure Javascript (ECMAScript 2015)以外のフレームワークを使っていません。これでSolrとのajax連携を含む、イベントドリブンモデルのサンプルになっています。
api.cgiは受け取ったクエリをSolrサーバに投げて返った結果を返すだけの単純なShell Scriptです。
bib.cgiが読み込む書誌ファイルであるlistfile.txtはファイル保存日時、HTMLファイルのパス、タイトル、ソースURL、カテゴリ、カテゴリに対応したインデックスHTMLファイルへのパスをTABで区切ったもので、ここでは単純にファイルを上から下まで舐めてます。この動作をこのサンプルでは最大30 (rowsMAX) 並列で投げて結果を待ち合わせて表示しています。これでも数千レコード程度であれば性能的に問題ないです。ちゃんと手間かけるなら書誌DBを作ったほうがいいのでしょうけれど。

このプログラムで実現されるWebブラウザによるクライアントはこのような形です。
2020051601
キーワードを入力して検索すると複数画面に分割して結果を表示します。Sub Queriesは、語をカンマで区切るとその区切りだけ絞り込み検索します。
2020051602

|

« ScrapBook及びSave Page WEで保存したWebページをApache Solrで検索できるようにしてみた(その1) | トップページ | Dockerを使ったJupyter Labの環境を用意する »

パソコン・インターネット」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)




« ScrapBook及びSave Page WEで保存したWebページをApache Solrで検索できるようにしてみた(その1) | トップページ | Dockerを使ったJupyter Labの環境を用意する »