20 Otsingumootori loomise jätk

Veebilehe kujundamine

Hetkel on meie projekti, mis on nähtav veebiaadressil 127.0.0.1:5000/teosed, täielikult stiliseerimata. Kujunduse loomine aitab projekti kasutamist mugavamaks teha ning pakub ka silmailu.

Alustame sellega, et loome projekti kausta sisse uue alamkausta static. Kausta static sisse loome alamkausta css ning selle sisse omakorda faili kujundus.css. Kuna tegemist on väikse projektiga, on mõistlik kasutada ühte kujunduse faili kõigi vahelehtede jaoks. Hetkel on meil vahelehti küll üks, kuid hiljem loome ühe juurde selleks, et näidata otsingute tulemusi.

Projekti kaust peaks hetkeseisuga välja nägema selline:

Projekti kausta ülesehitus 2

Projekti kausta alamkaust /static/css peaks välja nägema selline:

Projekti kausta alamkaust css

Ühtse kujunduse kasutamiseks kõigis failides tuleb kujunduse fail kujundus.css ühendada baasfailiga baas.html. Selleks lisame faili baas.html head siltide vahele järgmise rea:

<link rel="stylesheet" href="static/css/kujundus.css">

Asumegi kujundust looma. Käesolevas peatükis tuuakse jooksvalt näiteid, kuidas projekti kujundada võiks. Soovitatav on kõigepealt katsetada, milline näidiskujundus välja näeb ning seejärel võib projekti ka oma maitse ja eelistuste põhjal kujundama hakata.

Esmalt muudame veebilehe värve, määrame fondi, ääriseid ja muud. Selleks lisame faili kujundus.css järgnevad read:

html {
    background-color: #fdfdfd;
}

body {
    background-color: #fafafa;
    width: 1200px;
    font-family: Arial;
    padding: 20px;
    margin: 10px auto 5px auto;
    box-shadow: 0 0 8px #000000;
    border-radius: 10px;
}

Kui mõni kasutatud stiilielement jääb kas siin või tulevikus segaseks, otsi kindlasti nende kohta internetist infot. Üks soovitatav infoallikas on veebileht w3schools.

Kujundame ka teoste tabelit. Lisame sellele piirjooned, määrame veergude pealkirjade lahtritele teistsuguse taustavärvi ning lisame lahtritele täidiseid (ingl padding). Selleks lisame faili järgnevad read:

table, th, td {
    border: 1px solid black;
}

th, td {
    padding: 5px;
}

table {
    border-collapse: collapse;
}

th {
    background-color: #c9e4eb
}

Selleks, et hetkel kasutatud pealkirjad ning hiljem lisatavad tekstid oleksid veebilehe keskel, lisame juba praegu ka järgnevad read:

h1, h2, p {
    text-align: center;
}

Olemegi nüüdseks suure osa kujundamisest ära teinud, tänu millele on veebilehel kuvatavaid andmeid märksa mugavam vaadelda. Jätkame nüüd funktsionaalsuste lisamisega ning edaspidi täiendame kujunduse faili jooksvalt, siis kui selleks vajadus tekib.

Jooksuta enne jätkamist uuesti faili app.py ning tutvu veebirakenduse uue kujundusega.

Otsingufunktsiooni lisamine

Praeguseks on veebirakenduses näha kujundatud kujul Netflixi teoste infot. Seega saame keskenduda otsingufunktsioonide lisamisele. Selleks tuleb esmalt täiendada faili teosed.html. Nimelt tuleb sinna lisada veebivorm koos sisendiväljadega, mille põhjal koostatame andmebaasi päringuid.

Lisame faili teosed.html ploki body algusesse järgnevad read:

<h1>Otsingumootor</h1>
<form action='/tulemused' method='POST'>
    <div class="sisend">
        <label>Pealkiri:</label>
        <input type='text' name='o_pealkiri' id='o_pealkiri'>
    </div>
    <input type='submit' value='Otsi' class="nupp">
    <input type='reset' value='Tühjenda' class="nupp">
</form>
<br>

Nagu näha, lõime HTML-i form siltide abil veebivormi, kus on hetkel vaid üks sisendiväli (input siltide vahel) otsitava teose pealkirja jaoks. Lisaks on seal nupp Tühjenda, millele klikates tühjendatakse kõik veebivormi sisendiväljad ning nupp Otsi, millele vajutamisel suunatakse meid meetodiga POST aadressile 127.0.0.1:5000/tulemused. Kuna me pole sellist vahelehte veel loonud, tuleb praegu nupule Otsi vajutades veateade Not Found. Nupp Tühjenda juba töötab.

Kujundame loodud ja tulevasi sisendivälju. Lisame selleks faili kujundus.css järgnevad read:

input {
    padding: 4px;
}

.sisend {
    display: inline-block;
}

.sisend label {
    display: block;
}

Lisasime nii sisendiväljadele kui ka nuppudele täidised ning määrasime, et sisendivälja pealkirjad oleksid sisendiväljade kohal. Seda ka siis, kui sisendivälju hiljem juurde lisada. Otsi vajadusel internetist, mida tähendavad inline-block ja block. Nende mõistmine tuleb kindlasti igasugusel veebiarendusel kasuks.

Järgmisena täiendame faili app.py, kuhu kirjutame andmebaasi päringu koostamise loogika.

Alustuseks tuleb moodulist flask importida lisaks varasemale ka request. Seega peaks importide osa nüüdseks välja nägema selline:

from flask import Flask, render_template, request
from flask_sqlalchemy import SQLAlchemy

Seejärel lisame pärast meetodit teosed() järgnevad koodiread:

@app.route('/tulemused', methods=['GET', 'POST'])
def tulemused():
    tulemused = []
    o_pealkiri = request.form['o_pealkiri']
    tulemused = Teosed.query.filter(Teosed.pealkiri.like('%'+o_pealkiri+'%')).all()
    return render_template('tulemused.html', tulemused=tulemused)

Sellega näitame, mis juhtub, kui minna aadressile 127.0.0.1:5000/tulemused. Seal aadressil pole endiselt veel midagi, kuna selle sisu renderdatakse faili tulemused.html põhjal, mida me veel loonud ei ole. Teeme seda peatselt, kuid enne vaatame lisatud koodiread üle.

Reaga o_pealkiri = request.form['o_pealkiri'] väärtustame me muutuja o_pealkiri veebivormist saadud pealkirja sisendivälja väärtusega. Seejärel me loome päringu tabelist Teosed sellise piiranguga, et o_pealkiri väärtus peab esinema teose pealkirja sees. Pane tähele koodireas kasutatud märksõna like ning tuleta meelde, kuidas see PostgreSQL’s töötas.

Tasub teada, et siin ei eristata väärtuste puhul suur- ja väiketähti. Seega on näiteks pealkirja väärtused ‘love’ ja ‘LoVe’ siinkohal võrdsed.

Meetodi viimase rea abil renderdame aadressil 127.0.0.1:5000/tulemused faili tulemused.html ning anname selle faili argumendi tulemused väärtuseks päringust saadud tulemuste listi.

Nüüd on jäänud veel luua fail tulemused.html, pärast mida saamegi pealkirja põhjal otsimist katsetada. Faili tulemused.html ülesehitus on väga sarnane faili teosed.html algse ülesehitusega.

Lisame faili tulemused.html järgnevad read:

{% extends 'baas.html' %}
{% block head %} <title>Otsingu tulemused</title> {% endblock %}
{% block body %}
<h1>Otsingu tulemused</h1>
<table>
    <tr>
        <th>Pealkiri</th>
        <th>Tüüp</th>
        <th>Riigid</th>
        <th>Aasta</th>
        <th>Žanrid</th>
        <th>Näitlejad</th>
        <th>Kirjeldus</th>
    </tr>
    {% for teos in tulemused %}
        <tr>
            <td>{{ teos.pealkiri }}</td>
            <td>{{ teos.tuup }}</td>
            <td>{{ teos.riigid }}</td>
            <td>{{ teos.valjalaske_aasta }}</td>
            <td>{{ teos.zanrid }}</td>
            <td>{{ teos.naitlejad }}</td>
            <td>{{ teos.kirjeldus }}</td>
        </tr>
    {% endfor %}
</table>
{% endblock %}

Jooksuta nüüd uuesti faili app.py. Kirjuta seejärel midagi pealkirja sisendivälja ja kliki nupule Otsi ning sind suunatakse ümber aadressile 127.0.0.1:5000/tulemused, kus on näha kõiki teoseid, mis leiti sisestatud pealkirja väärtuse põhjal. Katseta otsimist erinevate sisendiväärtustega.

Nagu näha on ka veebirakenduse tulemuste vaheleht kujundatud. Seda mäletatavasti seetõttu, et ka failis tulemused.html viitame baasfailile baas.html, mis omakorda viitab kujunduse failile kujundus.css.

Mitme parameetri põhjal otsing

Kuigi pealkirja alusel otsimine täitsa töötab, oleks siiski mõistlik kasutajatele rohkem otsinguparameetreid pakkuda. Näiteks ei saa praeguse lahenduse korral otsida teoseid näitlejate või riikide põhjal.

Õnneks sarnaneb uute otsinguparameetrite lisamine varasemalt tehtuga. Alustame taaskord sellest, et lisame uued sisendiväljad faili teosed.html.

Lisame sisendiväljad tüübi, riikide, aasta, žanrite, näitlejate ja kirjelduse jaoks. Selleks kustutame failist teosed.html kõik form siltide vahel olevad koodiread, välja arvatud Otsi ja Tühjenda nuppude osad ning lisame kustutatud ridade asemele järgnevad read:

<h2>Täpsete parameetritega otsing</h2>
<div class="sisend">
    <label>Pealkiri:</label>
    <input type='text' name='o_pealkiri' id='o_pealkiri'>
</div>
<div class="sisend">
    <label for="o_tuup">Tüüp:</label>
    <select id="o_tuup" name="o_tuup">
        <option value="" selected>-</option>
        <option value="Movie">Movie</option>
        <option value="TV Show">TV Show</option>
    </select>
</div>
<div class="sisend">
    <label>Riigid:</label>
    <input type='text' name='o_riigid' id='o_riigid'>
</div>
<div class="sisend">
    <label>Aasta:</label>
    <input type='number' name='o_aasta' id='o_aasta'>
</div>
<div class="sisend">
    <label>Žanrid:</label>
    <input type='text' name='o_zanrid' id='o_zanrid'>
</div>
<div class="sisend">
    <label>Näitlejad:</label>
    <input type='text' name='o_naitlejad' id='o_naitlejad'>
</div>
<div class="sisend">
    <label>Kirjeldus:</label>
    <input type='text' name='o_kirjeldus' id='o_kirjeldus'>
</div>

Nagu näha, on enamik sisendivälju ülesehituse poolest pealkirja omaga identsed. Peamiste erinevustena lubame aasta jaoks vaid numbreid ning tüübi valiku lahendasime ripploendiga. Ripploendis pakume väärtustena võimalusi ‘Movie’, ‘TV Show’ ja ka vaikeväärtust ‘-‘ (mille tegelik väärtus on tühi sõne).

Selleks, et ka kasutatud ripploend ning varem lisatud nupud kenamad välja näeks, lisame faili kujundus.css järgnevad read:

select {
    padding: 5px;
}

.nupp {
    width: 80px;
    text-transform: uppercase;
}

Nüüd on taaskord tarvis täiendada app.py loogikat. Kuna meil on sisendivälju mitu, tahame, et otsingu tulemuses oleks vaid sellised teosed, mis vastavad kõigi sisendiväljade väärtustele. Seega on sisendiväljade väärtustel põhinevad piirangud vaja omavahel ühendada justkui märksõnaga AND. Siinkohal pole aga märksõna AND tarvis eraldi välja vaja kirjutada, piisab vaid sellest, kui eraldame piirangud omavahel komadega.

Muudame seega meetodit tulemused() järgnevalt:

@app.route('/tulemused', methods=['GET', 'POST'])
def tulemused():
    tulemused = []
    o_pealkiri = request.form['o_pealkiri']
    o_tuup = request.form['o_tuup']
    o_riigid = request.form['o_riigid']
    o_aasta = request.form['o_aasta']
    o_zanrid = request.form['o_zanrid']
    o_naitlejad = request.form['o_naitlejad']
    o_kirjeldus = request.form['o_kirjeldus']
    tulemused = Teosed.query.filter(
        Teosed.pealkiri.like('%'+o_pealkiri+'%'),
        Teosed.tuup.like('%'+o_tuup+'%'),
        Teosed.riigid.like('%'+o_riigid+'%'),
        Teosed.valjalaske_aasta.like('%'+o_aasta+'%'),
        Teosed.zanrid.like('%'+o_zanrid+'%'),
        Teosed.naitlejad.like('%'+o_naitlejad+'%'),
        Teosed.kirjeldus.like('%'+o_kirjeldus+'%')
    ).all()
    return render_template('tulemused.html', tulemused=tulemused)

Nagu näha, väärtustasime kõigepealt kõigi sisendiväljade põhjal neile vastavad muutujad. Seejärel kasutasime neid kõiki ka päringu piiramiseks, eraldades need omavahel komadega.

Jooksuta nüüd uuesti faili app.py, kirjuta midagi erinevatesse sisendiväljadesse ning kliki nupule Otsi. Taaskord suunatakse sind ümber aadressile 127.0.0.1:5000/tulemused, kuid seekord põhineb otsingu tulemus kõigil sisenditel, kuhu mingi väärtus sisestati. Kui piirangud on liiga kitsad, võib tulemuste tabel ka tühi olla.

Laia otsingu lisamine

Proovime nüüd lisada ka uut Google’ga sarnast otsingut. Selleks lisame ühe eraldiseisva suure sisendivälja.

Kui kasutaja kirjutab midagi suurde sisendivälja ning klikib nupule Otsi, võrreldakse sisendivälja väärtust erinevate meie valitud tunnustega. Kasutame siinkohal võrdluse aluseks tunnuseid pealkiri, žanrid, näitlejad ja kirjeldus.

Esimese sammuna täiendame jällegi faili teosed.html. Lisame form siltide vahele, pärast nuppe Otsi ja Tühjenda, järgnevad read:

<h2>Lai otsing</h2>
<p>Kui kasutad laia otsingut, siis täpse otsingu parameetreid arvesse ei võeta.<p>
<input type='text' name='o_lai' id='o_lai'>
<input type='submit' value='Otsi' class="nupp">

Tegemist on tavapärase sisendiväljaga, mille kõrval on eraldi otsingunupp. Tegelikkuses on see nupp siiski samaväärne juba varem lisatud otsingunupuga ning funktsionaalsuse poolest ei ole vahet, kummale neist klikata.

Selleks, et uus sisendiväli tõepoolest suur ja lai oleks, lisame faili kujundus.css järgnevad read:

#o_lai {
    width: 1100px;
}

Sellega on ühtlasi kujundamise osa läbi ning siinkohal võib faili kujundus.css sulgeda, kui pole just soovi otsingumootori välimust ka ise kohandada.

Nüüd on jäänud täiendada fail app.py. Lahendame siinkohal olukorra, kumba otsingut peaks programm Otsi nuppudele vajutamisel kasutama, mõnevõrra naiivselt. Nimelt, kontrollime, kas laia otsingu sisendivälja on midagi kirjutatud ja kui on, siis kasutame laia otsingu loogikat. Vastasel juhul kasutame täpsetel parameetritel põhineva otsingu loogikat.

Laia otsingu funktsionaalsuse lisamiseks ning otsingute vahel valimiseks muudame meetodit tulemused() järgnevalt:

@app.route('/tulemused', methods=['GET', 'POST'])
def tulemused():
    tulemused = []
    o_lai = request.form['o_lai']
    if o_lai != "":
        tulemused = Teosed.query.filter(
            (Teosed.pealkiri.like('%'+o_lai+'%')) |
            (Teosed.zanrid.like('%'+o_lai+'%')) |
            (Teosed.naitlejad.like('%'+o_lai+'%')) |
            (Teosed.kirjeldus.like('%'+o_lai+'%'))
        ).all()
    else:
        tulemused = []
        o_pealkiri = request.form['o_pealkiri']
        o_tuup = request.form['o_tuup']
        o_riik = request.form['o_riik']
        o_aasta = request.form['o_aasta']
        o_zanrid = request.form['o_zanrid']
        o_naitlejad = request.form['o_naitlejad']
        o_kirjeldus = request.form['o_kirjeldus']
        tulemused = Teosed.query.filter(
            Teosed.pealkiri.like('%'+o_pealkiri+'%'),
            Teosed.tuup.like('%'+o_tuup+'%'),
            Teosed.riigid.like('%'+o_riigid+'%'),
            Teosed.riigid.like('%'+o_aasta+'%'),
            Teosed.zanrid.like('%'+o_zanrid+'%'),
            Teosed.naitlejad.like('%'+o_naitlejad+'%'),
            Teosed.kirjeldus.like('%'+o_kirjeldus+'%')
        ).all()
    return render_template('tulemused.html', tulemused=tulemused)

Parameetrite põhjal otsingu osa jäi samaks, kuid nüüd on juures ka laia otsingu loogika ning kontroll, kumba otsingut kasutada.

Vaatame täpsemalt üle lisatud laia otsingu loogikat. Kõigepealt on näha, et laia otsingu sisendivälja väärtust võrreldakse nii pealkirja, žanrite, näitlejate kui ka kirjeldusega. Soovi korral võib võrdlemise aluseid ka muuta.

Kuna me soovime laia otsingu abil leida kõigi piirangute põhjal leitud tulemuste ühendit, oleks meil piirangute vahel vaja kasutada justkui märksõna OR. Flask-SQLAlchemy päringutes paneme selle jaoks iga üksiku piirangu ümber sulud ning eraldame need omavahel sümboliga ‘|’.

Tasub teada, et enamustes programmeerimiskeeles (kuid mitte Python’is) kasutatakse loogilise operaatori or tähistamiseks sümbolit ‘|’ või sümboleid ‘||’.

Jooksuta nüüd uuesti faili app.py, kirjuta midagi laia otsingu sisendivälja ja kliki nupul Otsi. Seejärel peaksid nägema teoseid, mida leiti nii pealkirja, žanrite, näitlejate kui ka kirjelduse alusel. Katseta erinevaid väärtusi ning veendu, et kirjutades midagi laia otsingu sisendivälja, kasutatakse just laia otsingu loogikat, mitte täpsete parameetritega otsingu oma.

Sellega on otsingumootori projekt valmis, kuigi täiendusi, mis aitavad õpitut veelgi enam kinnistada, on alati võimalik teha.

Ühtlasi oleme jõudnud valikmooduli “Andmebaasi päringud” lõppu. Siiski, jäänud on veel lisalugemise peatükk vaadete kohta, mis aitab paremini mõista, kuidas suurtes andmebaasides ning mitmetes tabelites hoiustatavaid andmeid internetis huvitaval kujul kuvatakse.

Litsents

Icon for the Creative Commons Attribution 4.0 International License

Lisamoodulid on loodud Aveli Klaos, Siim Tanel Laisaar, Piret Luik, Tauno Palts, ja Eero Ääremaa poolt Creative Commons Attribution 4.0 International License litsentsi alusel, kui pole teisiti märgitud.

Jaga seda raamatut