[ElasticSearch] 검색엔진 만들기 4 – Flask 웹서비스
elasticsearch에 필요한 데이터는 다 색인해뒀기 때문에 검색엔진 자체는 다 만들어졌고 서비스를 위해서 적당한 UI 만 있으면 될 것 같다. 기존 운영하는 웹페이지에 이식하고자 하는 경우에는 작게 모듈형태로 만들어서 간단한 스크립트로 해당 검색 엔진을 불러오게할 수도 있을 것 같은데 이것만으로 완전한 독립 서비스를 구축하는 것이어서 크게 단계를 나누어서 만들지는 않았다.
flask 만으로 웹페이지를 만들기 위해서 아래와 같은 구성요소들을 이용한다. 앞서 만들어놓은 search.py
외에 app.py
, hlight.py
, templates/
, static/
이 추가되는데
├── app.py
├── body.json
├── hlight.py
├── search.py
├── static
│ ├── css
│ │ ├── style.css
└── templates
├── index.html
└── result.html
hlight.py
는 단순히 검색 결과에 강조 효과를 더해주는 간단한 함수일 뿐이고, app.py
가 이 프로젝트의 메인으로 웹서버 역할과 웹페이지 렌더링 등을 담당한다. templates/
, static/
디렉토리는 flask 에서 웹 리소스를 로드하는데에 사용되는 경로인데 무조건 templates/
, static/
으로 이름지어야 한다.
app.py
index()
에서 기본적인 검색 화면과 검색 결과를 모두 서비스하고 issueView()
에서는 사용자가 선택한 화면으로 redirect만 수행한다. 굳이 issueView()
함수를 추가한 이유는 검색 결과에서 단순히 링크로 해당 이슈로 넘어가도록 했을 때에는 별도 로깅을 하지 않는 한 웹서버 accesslog 에서 트래킹을 할 수 없어 사용자 행동 추적을 위해 추가했다.
request method (GET
,POST
) 에 따라서 다른 결과를 리턴하게 되는데 이를 이용해서 검색어를 입력받았을 때와 처음 웹페이지에 접근했을 때 그리고 검색결과를 뿌려줄 때를 구분할 수 있다.
#!/bin/python3.6
import os
from flask import Flask, flash, render_template, request, redirect, session, make_response
import search
import hlight
App = Flask(__name__)
App.config["DEBUG"] = True
App.secret_key = 'dong1lkim, University of Seoul'
@App.route('/', methods=['GET','POST',])
def index():
if request.method == 'POST':
rctgr = request.form.getlist('ctgr')
ctgr = rctgr.pop(0)
for c in rctgr: ctgr += ',' + c
sort = request.form['sort'] if 'sort' in request.form else ''
product = request.form['product']
return redirect('?s='+request.form['s']+'&product='+product+'&sort='+sort+'&ctgr='+ctgr)
s = request.args['s'] if 's' in request.args else None
p = int(request.args['p']) if 'p' in request.args else 1
sort = request.args['sort'] if 'sort' in request.args else None
ctgr = request.args['ctgr'].split(',') if 'ctgr' in request.args else None
product = request.args['product'] if 'product' in request.args else None
if s == None: return render_template('index.html')
rctgr,temp = list(ctgr),list(ctgr)
ctgr = temp.pop(0)
for c in temp: ctgr += ',' + c
result,total = search.issue(s,page=p,product=product,sort=sort,ctgr=rctgr)
pagination = {'current':p,"total":total//10,"max":total}
opts = {'sort':sort,'product':product,'ctgr':ctgr,'rctgr':rctgr}
if total == 0: result,total = search.issue(s,page=p,product=product,sort=sort,ctgr=rctgr,op='or')
for i in result: i['_source'].update({'Issue Details':hlight.hlight_term(i['_source']['Issue Details'],s.split())})
return make_response(
render_template('result.html',s=s,pagination=pagination,result=result,opts=opts,hist=[])
)
@App.route('/issueView', methods=['GET'])
def issueView():
issueId = request.args['issueId']
return redirect('https://ims.tmaxsoft.com/tody/ims/issue/issueView.do?issueId='+issueId)
if __name__ == '__main__': App.run(host='0.0.0.0',port='8888')
request args 를 이용해서 웹 페이지 form 에 입력된 값들을 서버와 통신하도록 설계했기때문에 이 request 모델이 바뀔 때마다 웹페이지 템플릿을 맞춰줘야하는 번거로움이 있었다. 마찬가지로 템플릿을 변경하더라도 웹서버가 제대로 응답해줄 수 있는지 확인하는 작업이 필요하다.
flask.session
을 활용하면 로그인 기능이라던지 쿠키 기능 등을 활용할 수 있다.
hlight.py
이 함수는 위 app.py
에서 검색 결과를 리턴할 때에 호출되는데, 단순히 검색 결과에 검색어가 포함되는 위치를 찾아 해당 검색어 부근의 500자만 리턴한다.
import re
def hlight_term(string,term):
if len(term) < 2:
idx = string.find(term[0])
begin = max(18,idx-10)
pattern = "r'("+term[0]+")'"
result = string[begin:begin+500]
return result
else:
begin = 400
for i in range(len(term)):
begin = min(begin,string.find(term[i]))
begin = max(18,begin)
result = string[begin:begin+500]
return result
index.html
위 app.py
에서 request agrs 가 아무것도 없을 때 렌더링 하게 되는 템플릿이다.
<!DOCTYPE html>
<html lang="ko-KR">
<head>
<meta charset="UTF-8">
<title>Search for IMS – tbig</title>
<script type="text/javascript">
var bDisplay = true;
function doDisplay(){
var con = document.getElementById("sOpts");
if(con.style.display == 'none'){
con.style.display = 'block';
}
else{
con.style.display = 'none';
}
}
</script>
<style>
#sOptsOrder li,ul {
list-style:none;
margin: 0 0 0 0;
padding: 0 0 0 0;
}
#sOpts select[multiple]{
height:120px;
}
#sOpts select[multiple]::-webkit-scrollbar{
display:none;
}
</style>
</head>
<body>
<main>
<header>
<div>
<a href="/"><h1>Search for IMS</h1></a>
</div>
</header>
<section>
<div>
<h2>Search</h2><hr>
<form method="POST">
<p>
<label>Keyword</label>
<input type="text" name="s" value="" placeholder="Type your search term here" maxlength="90%">
</p>
<div id="sOptsOrder">
<li>
<ul><input type="radio" name="sort" value="accuracy" checked><label>order by score</label></ul>
<ul><input type="radio" name="sort" value="latest"><label>order by registered date</label></ul>
</li><br>
</div>
<div id="sOpts" style="display:none; -ms-overflow-style:none;">
<label>Product</label>
<select name="product">
<option value="All">All</option>
<option value="API Gateway" >API Gateway</option>
<option value="AnyLink Over Tmax" >AnyLink Over Tmax</option>
<option value="AnyMiner" >AnyMiner</option>
<option value="BizMaster" >BizMaster</option>
<option value="Bridge" >Bridge</option>
<option value="CDS" >CDS</option>
<option value="HyperData" >HyperData</option>
<option value="HyperLoader" >HyperLoader</option>
<option value="InfiniCache" >InfiniCache</option>
<option value="InfiniLink" >InfiniLink</option>
<option value="JEUS" >JEUS</option>
<option value="OpenFrame" >OpenFrame</option>
<option value="ProBus" >ProBus</option>
<option value="ProCMS" >ProCMS</option>
<option value="ProFrame C" >ProFrame C</option>
<option value="ProFrame Java" >ProFrame Java</option>
<option value="ProGroup" >ProGroup</option>
<option value="ProObject 7 Framework" >ProObject 7 Framework</option>
<option value="ProObject 7 Runtime" >ProObject 7 Runtime</option>
<option value="ProPaaS" >ProPaaS</option>
<option value="ProSync" >ProSync</option>
<option value="ProVDI" >ProVDI</option>
<option value="ProZone" >ProZone</option>
<option value="SysMaster" >SysMaster</option>
<option value="TOP" >TOP</option>
<option value="Tibero" >Tibero</option>
<option value="Tmax" >Tmax</option>
<option value="Tmax OS" >Tmax OS</option>
<option value="Tmax Office" >Tmax Office</option>
<option value="TmaxRQS" >TmaxRQS</option>
<option value="TmaxWorkSpace" >TmaxWorkSpace</option>
<option value="ToGate" >ToGate</option>
<option value="WebtoB" >WebtoB</option>
<option value="ZetaData" >ZetaData</option>
</select>
<label>Category</label>
<select name="ctgr" multiple>
<option value="Change Request" selected>Change Request</option>
<option value="Enhancement Request" selected>Enhancement Request</option>
<option value="Technical Support" selected>Technical Support</option>
<option value="Defect" selected>Defect</option>
<option value="Binary Request">Binary Request</option>
</select>
<input type=submit value=Search>
</div>
<a href="javascript:doDisplay();">검색 옵션</a>
</form>
</div>
<aside>
<div>
<aside>
<h4>About</h4>
</aside>
</div>
</aside>
</section>
<footer>
<div></div>
</footer>
</main>
</body>
</html>
result.html
약속된 request args 가 입력됐을 때에 리턴되는 페이지로, 검색창 외에 검색 결과와 pagination을 위한 navigator 가 추가돼있다.
<!DOCTYPE html>
<html lang="ko-KR">
<head>
<meta charset="UTF-8">
<title>{{ s }} – Search for IMS</title>
<script type="text/javascript">
var bDisplay = true;
function doDisplay(){
var con = document.getElementById("sOpts");
if(con.style.display == 'none'){
con.style.display = 'block';
}
else{
con.style.display = 'none';
}
}
</script>
<style>
#sOptsOrder li,ul {
list-style:none;
margin: 0 0 0 0;
padding: 0 0 0 0;
}
#sOpts select[multiple]{
height:120px;
}
#sOpts select[multiple]::-webkit-scrollbar{
display:none;
}
</style>
</head>
<body>
<main>
<header>
<div>
<a href="/"><h1 class="white mb-0">Search for IMS</h1></a>
</div>
</header>
<section>
<div>
<p>검색결과: {{ pagination['max'] }} 건</p>
{% for iss in result %}
<article>
<div>
<ul>
<li>
<span>{{ iss['_source']['Registered date'] }}</span>
</li>
<li>
<span>in </span>
<a href="#">{{ iss['_source']['Customer'] }}</a> | <a href="#" class="entry-category">{{ iss['_source']['Project'] }}</a>
</li>
<li>
<a href="#">{{ iss['_source']['Reporter'] }}</a></li>
<li>
<a href="#">{{ iss['_source']['Product'] }} {{ iss['_source']['Version'] }}</a> | <a href="#">{{ iss['_source']['Module'] }}</a> | <a href="#">{{ iss['_source']['Category'] }}</a>
</li>
</ul>
<h2><a href="/issueView?issueId={{ iss['_source']['Issue Number'] }}" target="_blank">{{ iss['_source']['Issue Number'] }} - {{ iss['_source']['Subject'] }}</a></h2>
<div>
<a href="/issueView?issueId={{ iss['_source']['Issue Number'] }}"target="_blank">https://ims.tmaxsoft.com/tody/ims/issue/issueView.do?issueId={{ iss['_source']['Issue Number'] }}</a>
</div>
<div>
<p>{{ iss['_source']['Issue Details'] }}</p>
</div>
<ul>
<li>
<span>SCORE: {{ iss['_score'] }}</span>
</li>
</ul>
</div>
</article>
{% endfor %}
<nav>
<h2>페이지 탐색</h2>
<div>
{% if pagination['current'] <= 4 %}
{% if pagination['total'] <= 8 %}
{% for p in range(1,pagination['total']+1) %}
{% if p == pagination['current'] %}
<span aria-current='page' class='page-numbers current'>{{ p }}</span>
{% else %}
<a class='page-numbers' href='?s={{ s }}&product={{ opts['product'] }}&p={{ p }}&sort={{ opts['sort'] }}&ctgr={{ opts['ctgr'] }}'>{{ p }}</a>
{% endif %}
{% endfor %}
{% else %}
{% for p in range(1,9) %}
{% if p == pagination['current'] %}
<span aria-current='page' class='page-numbers current'>{{ p }}</span>
{% else %}
<a class='page-numbers' href='?s={{ s }}&product={{ opts['product'] }}&p={{ p }}&sort={{ opts['sort'] }}&ctgr={{ opts['ctgr'] }}'>{{ p }}</a>
{% endif %}
{% endfor %}
{% endif %}
{% else %}
{% if pagination['total'] > pagination['current']+4 %}
{% for p in range(pagination['current']-3,pagination['current']+5) %}
{% if p == pagination['current'] %}
<span aria-current='page' class='page-numbers current'>{{ p }}</span>
{% else %}
<a class='page-numbers' href='?s={{ s }}&product={{ opts['product'] }}&p={{ p }}&sort={{ opts['sort'] }}&ctgr={{ opts['ctgr'] }}'>{{ p }}</a>
{% endif %}
{% endfor %}
{% else %}
{% for p in range(pagination['total']-5,pagination['total']+1) %}
{% if p == pagination['current'] %}
<span aria-current='page' class='page-numbers current'>{{ p }}</span>
{% else %}
<a class='page-numbers' href='?s={{ s }}&product={{ opts['product'] }}&p={{ p }}&sort={{ opts['sort'] }}&ctgr={{ opts['ctgr'] }}'>{{ p }}</a>
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
</div>
</nav>
</div>
<aside>
<div>
<aside>
<h4>Search</h4>
<div>
<form method="POST">
<p>
<input type="text" name="s" value="{{ s }}">
</p>
<div id="sOptsOrder">
<li>
{% if opts['sort']=='latest' %}
<ul><input type="radio" name="sort" value="accuracy"><label>order by score</label></ul>
<ul><input type="radio" name="sort" value="latest" checked><label>order by registered date</label></ul>
{% else %}
<ul><input type="radio" name="sort" value="accuracy" checked><label>order by score</label></ul>
<ul><input type="radio" name="sort" value="latest"><label>order by registered date</label></ul>
{% endif %}
</li><br>
</div>
<div id="sOpts" style="display:none">
<label>Product</label>
<select name="product">
<option value="All">All</option>
{% if opts['product'] == 'API Gateway' %}<option value="API Gateway" selected>API Gateway</option>
{% else %}<option value="API Gateway" >API Gateway</option>{% endif %}
{% if opts['product'] == 'AnyLink Over Tmax' %}<option value="AnyLink Over Tmax" selected>AnyLink Over Tmax</option>
{% else %}<option value="AnyLink Over Tmax" >AnyLink Over Tmax</option>{% endif %}
{% if opts['product'] == 'AnyMiner' %}<option value="AnyMiner" selected>AnyMiner</option>
{% else %}<option value="AnyMiner" >AnyMiner</option>{% endif %}
{% if opts['product'] == 'BizMaster' %}<option value="BizMaster" selected>BizMaster</option>
{% else %}<option value="BizMaster" >BizMaster</option>{% endif %}
{% if opts['product'] == 'HyperData' %}<option value="HyperData" selected>HyperData</option>
{% else %}<option value="HyperData" >HyperData</option>{% endif %}
{% if opts['product'] == 'HyperLoader' %}<option value="HyperLoader" selected>HyperLoader</option>
{% else %}<option value="HyperLoader" >HyperLoader</option>{% endif %}
{% if opts['product'] == 'InfiniCache' %}<option value="InfiniCache" selected>InfiniCache</option>
{% else %}<option value="InfiniCache" >InfiniCache</option>{% endif %}
{% if opts['product'] == 'InfiniLink' %}<option value="InfiniLink" selected>InfiniLink</option>
{% else %}<option value="InfiniLink" >InfiniLink</option>{% endif %}
{% if opts['product'] == 'JEUS' %}<option value="JEUS" selected>JEUS</option>
{% else %}<option value="JEUS" >JEUS</option>{% endif %}
{% if opts['product'] == 'JMaker' %}<option value="JMaker" selected>JMaker</option>
{% else %}<option value="JMaker" >JMaker</option>{% endif %}
{% if opts['product'] == 'K-DB' %}<option value="K-DB" selected>K-DB</option>
{% else %}<option value="K-DB" >K-DB</option>{% endif %}
{% if opts['product'] == 'OpenFrame' %}<option value="OpenFrame" selected>OpenFrame</option>
{% else %}<option value="OpenFrame" >OpenFrame</option>{% endif %}
{% if opts['product'] == 'ProObject 7 Framework' %}<option value="ProObject 7 Framework" selected>ProObject 7 Framework</option>
{% else %}<option value="ProObject 7 Framework" >ProObject 7 Framework</option>{% endif %}
{% if opts['product'] == 'ProObject 7 Runtime' %}<option value="ProObject 7 Runtime" selected>ProObject 7 Runtime</option>
{% else %}<option value="ProObject 7 Runtime" >ProObject 7 Runtime</option>{% endif %}
{% if opts['product'] == 'ProPaaS' %}<option value="ProPaaS" selected>ProPaaS</option>
{% else %}<option value="ProPaaS" >ProPaaS</option>{% endif %}
{% if opts['product'] == 'ProSync' %}<option value="ProSync" selected>ProSync</option>
{% else %}<option value="ProSync" >ProSync</option>{% endif %}
{% if opts['product'] == 'ProSync Manager' %}<option value="ProSync Manager" selected>ProSync Manager</option>
{% else %}<option value="ProSync Manager" >ProSync Manager</option>{% endif %}
{% if opts['product'] == 'ProVDI' %}<option value="ProVDI" selected>ProVDI</option>
{% else %}<option value="ProVDI" >ProVDI</option>{% endif %}
{% if opts['product'] == 'SysMaster' %}<option value="SysMaster" selected>SysMaster</option>
{% else %}<option value="SysMaster" >SysMaster</option>{% endif %}
{% if opts['product'] == 'TOP' %}<option value="TOP" selected>TOP</option>
{% else %}<option value="TOP" >TOP</option>{% endif %}
{% if opts['product'] == 'Tibero' %}<option value="Tibero" selected>Tibero</option>
{% else %}<option value="Tibero" >Tibero</option>{% endif %}
{% if opts['product'] == 'Tmax' %}<option value="Tmax" selected>Tmax</option>
{% else %}<option value="Tmax" >Tmax</option>{% endif %}
{% if opts['product'] == 'Tmax OS' %}<option value="Tmax OS" selected>Tmax OS</option>
{% else %}<option value="Tmax OS" >Tmax OS</option>{% endif %}
{% if opts['product'] == 'Tmax Office' %}<option value="Tmax Office" selected>Tmax Office</option>
{% else %}<option value="Tmax Office" >Tmax Office</option>{% endif %}
{% if opts['product'] == 'TmaxRQS' %}<option value="TmaxRQS" selected>TmaxRQS</option>
{% else %}<option value="TmaxRQS" >TmaxRQS</option>{% endif %}
{% if opts['product'] == 'TmaxWorkSpace' %}<option value="TmaxWorkSpace" selected>TmaxWorkSpace</option>
{% else %}<option value="TmaxWorkSpace" >TmaxWorkSpace</option>{% endif %}
{% if opts['product'] == 'ToGate' %}<option value="ToGate" selected>ToGate</option>
{% else %}<option value="ToGate" >ToGate</option>{% endif %}
{% if opts['product'] == 'WebtoB' %}<option value="WebtoB" selected>WebtoB</option>
{% else %}<option value="WebtoB" >WebtoB</option>{% endif %}
{% if opts['product'] == 'ZetaData' %}<option value="ZetaData" selected>ZetaData</option>
{% else %}<option value="ZetaData" >ZetaData</option>{% endif %}
</select>
<label>Category</label>
<select name="ctgr" multiple>
{% if 'Change Request' in opts['rctgr'] %}<option value="Change Request" selected>Change Request</option>
{% else %}<option value="Change Request">Change Request</option>{% endif %}
{% if 'Enhancement Request' in opts['rctgr'] %}<option value="Enhancement Request" selected>Enhancement Request</option>
{% else %}<option value="Enhancement Request">Enhancement Request</option>{% endif %}
{% if 'Technical Support' in opts['rctgr'] %}<option value="Technical Support" selected>Technical Support</option>
{% else %}<option value="Technical Support">Technical Support</option>{% endif %}
{% if 'Defect' in opts['rctgr'] %}<option value="Defect" selected>Defect</option>
{% else %}<option value="Defect">Defect</option>{% endif %}
{% if 'Binary Request' in opts['rctgr'] %}<option value="Binary Request" selected>Binary Request</option>
{% else %}<option value="Binary Request">Binary Request</option>{% endif %}
</select>
<input type=submit value=Search>
</div>
<a href="javascript:doDisplay();">검색 옵션</a>
</form>
</div>
</aside>
</div>
</aside>
</section>
<footer>
<div></div>
</footer>
</main>
</body>
</html>