Slowly remove DSL and split frontend into components

This commit is contained in:
Sijmen 2018-10-19 20:23:52 +02:00
parent 0ebb5f7013
commit 3cd7b78d81
11 changed files with 329 additions and 243 deletions

0
backend/__init__.py Normal file
View File

View File

@ -1,62 +1,26 @@
from datetime import datetime
from flask import Flask, jsonify, request
from elasticsearch_dsl import Document, Date, Integer, Keyword, Text, FacetedSearch, TermsFacet, DateHistogramFacet
from elasticsearch_dsl import Search
from elasticsearch_dsl.connections import connections
from tqdm import tqdm
from beeprint import pp
import csv
import click
import re
class Question(Document):
title = Text(analyzer="snowball")
body = Text(analyzer="snowball")
category = Keyword()
date = Date()
from .question import Question
from .question_search import QuestionSearch
class Index:
name = "goeievraag"
def save(self, **kwargs):
return super(Question, self).save(**kwargs)
@property
def url(self):
id_ = self.meta.id
if not self.category:
return f"https://www.startpagina.nl/v/vraag/{id_}"
category = self.category.lower()
category = re.sub("[^A-Za-z ]", "", category).replace(" ", "-")
return f"https://www.startpagina.nl/v/{category}/vraag/{id_}"
@property
def summary(self, length=128):
if len(self.body) > length:
return self.body[:length - 3] + "..."
return self.body
class QuestionSearch(FacetedSearch):
doc_types = Question,
fields = "category", "title", "body"
facets = {
"date_frequency": DateHistogramFacet(field="date", interval="month"),
"category": TermsFacet(field="category")
}
app = Flask(__name__)
connections.create_connection(hosts=["localhost"])
Question.init()
def round_sigfig(value, figures):
return float(format(value, f".{figures}g"))
@app.route("/api/")
def index():
def round_sigfig(value, figures):
return float(format(value, f".{figures}g"))
query = request.args.get("q")
categories = request.args.get("categories", None)
@ -65,19 +29,41 @@ def index():
category_list = categories.split(",")
facets["category"] = category_list
search = QuestionSearch(query, facets)
search = Search.from_dict({
"query": {
"query_string": {
"query": query,
},
},
"aggregations": {
"category": {
"terms": {"field": "category"},
},
},
})
response = search.execute()
date_facets = [{"timestamp": date.timestamp(), "count": count}
for date, count, _ in response.facets.date_frequency]
category_facets = [{"category": category, "count": round_sigfig(count, 3)}
for category, count, _ in response.facets.category]
#date_facets = [{"timestamp": date.timestamp(), "count": count}
#for date, count, _ in response.facets.date_frequency]
category_facets = [
{"category": bucket.key, "count": round_sigfig(bucket.doc_count, 3)}
for bucket in response.aggregations.category.buckets
]
results = [{"id": hit.meta.id, "score": hit.meta.score, "title": hit.title,
"body": hit.summary, "category": hit.category,
"date": hit.date, "url": hit.url}
for hit in response]
date_facets = []
results = []
for hit in response:
summary = Question.summary(hit)
url = Question.url(hit)
results.append({
"id": hit.meta.id, "score": hit.meta.score,
"title": hit.title, "body": summary,
"category": hit.category, "date": hit.date,
"url": url,
})
return jsonify(
facets={"months": date_facets, "categories": category_facets},

24
backend/question.py Normal file
View File

@ -0,0 +1,24 @@
from elasticsearch_dsl import Document, Date, Keyword, Text
class Question(Document):
title = Text(analyzer="snowball")
body = Text(analyzer="snowball")
category = Keyword()
date = Date()
class Index:
name = "goeievraag"
def save(self, **kwargs):
return super(Question, self).save(**kwargs)
def url(self):
id_ = self.meta.id
return f"https://www.startpagina.nl/v/vraag/{id_}/"
def summary(self, length=128):
if len(self.body) > length:
return self.body[:length - 3] + "..."
return self.body

View File

@ -0,0 +1,12 @@
from elasticsearch_dsl import FacetedSearch, TermsFacet, DateHistogramFacet
from .question import Question
class QuestionSearch(FacetedSearch):
doc_types = Question,
fields = "title", "body"
facets = {
"date_frequency": DateHistogramFacet(field="date", interval="month"),
"category": TermsFacet(field="category"),
}

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>zoekmachine</title>
<title>Goeievraagle</title>
</head>
<body>
<div id="app"></div>

View File

@ -14,6 +14,10 @@ body {
font-size: 13px;
}
a {
text-decoration: none;
}
#app {
font-family: Arial, sans-serif;
color: #2c3e50;

View File

@ -4,15 +4,33 @@
<img class="logo__image" src="@/assets/logo.png">
</div>
<div class="query">
<input class="query__input">
<input class="query__input" v-model="query" @keyup.enter="search">
<div class="query__buttons">
<input type="button" value="Goeievraagle zoeken" class="query__submit">
<input type="button" value="Ik doe een gok" class="query__submit">
<input type="button" value="Goeievraagle zoeken" class="query__submit" @click="search">
<input type="button" value="Ik doe een gok" class="query__submit" @click="lucky">
</div>
</div>
</div>
</template>
<script>
import Vue from "vue";
import {Component} from "vue-property-decorator";
@Component
export default class Index extends Vue {
query = "";
search() {
const query = encodeURIComponent(this.query);
this.$router.push(`/search?q=${query}`);
}
lucky() {
}
}
</script>
<style scoped>
.index {
text-align: center;

View File

@ -0,0 +1,174 @@
<template>
<div class="body">
<div class="result-count">
Ongeveer {{hits}} resultaten ({{responseTime.toFixed(2)}} seconden)
</div>
<div class="body__inner">
<div class="body__facets">
<div v-for="facet in facets.categories" v-bind:key="facet.category"
v-bind:class="{active: activeCategories.includes(facet.category)}"
class="category facet" @click="toggleCategory(facet)">
<div class="category__facet">{{facet.category}}</div>
<div v-if="facet.count < 10000" class="category__count">{{facet.count}}</div>
<div v-else class="category__count">{{(facet.count / 1000).toFixed()}}K</div>
</div>
</div>
<div class="body__results">
<div v-for="result in results" v-bind:key="result.id" class="result">
<a :href="result.url" target="_blank">
<div class="result__title">{{result.title}}</div>
<div class="result__link">
{{result.url}} &bullet; {{result.category}} &bullet; {{result.score.toFixed(3)}}
</div>
<div class="result__body">
<span class="result__date">{{result.date}} -</span>
{{result.body}}
</div>
</a>
</div>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue";
import {Component, Prop, Watch} from "vue-property-decorator";
@Component
export default class ResultBody extends Vue {
@Prop() query;
activeCategories = [];
hits = 0;
results = [];
responseTime = 0;
facets = {};
@Watch("query")
async onQueryChanged(value) {
await this.search();
}
async toggleCategory(category) {
const name = category.category;
const index = this.activeCategories.indexOf(name);
if (index === -1) {
this.activeCategories.push(name);
} else {
this.activeCategories.splice(index, 1);
}
await this.search();
}
async search() {
const query = encodeURIComponent(this.query);
let queryString = `?q=${query}`;
if (this.activeCategories.length > 0) {
const categories = encodeURIComponent(this.activeCategories.join(","));
queryString += `&categories=${categories}`;
}
this.$router.history.push(`/search${queryString}`);
const url = `/api/${queryString}`;
let response = await fetch(url);
let json = await response.json();
this.results = json.results;
this.hits = json.hits;
this.responseTime = json.took;
this.facets = json.facets;
}
}
</script>
<style scoped>
.body {
width: 100%;
}
.body__inner {
display: flex;
}
.result-count {
line-height: 43px;
color: rgba(0, 0, 0, 0.5);
margin-left: 208px;
}
.result {
margin: 24px 0;
max-width: 600px;
}
.result:first-child {
margin-top: 12px;
}
.result__title {
font-size: 18px;
margin: 1px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.result__link {
font-size: 14px;
color: #006621;
}
.result__body {
margin: 4px 0;
color: rgba(0, 0, 0, .68);
}
.result__date {
color: rgba(0, 0, 0, .5);
}
.body__facets {
width: 184px;
margin-right: 24px;
}
.facet {
padding: 4px 8px;
cursor: pointer;
}
.facet:hover {
background-color: rgba(0, 0, 0, .1);
}
.facet.active {
font-weight: bold;
}
.category {
display: flex;
justify-content: space-between;
}
.category__facet {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 8px;
}
.category__count {
text-align: right;
}
</style>

View File

@ -1,202 +1,25 @@
<template>
<div>
<div class="topbar">
<div class="logo">
<img class="logo__image" src="@/assets/logo.png">
</div>
<div class="query">
<input class="query__input" v-model="query" @change="getResults">
</div>
</div>
<div class="body">
<div class="result-count">
Ongeveer {{hits}} resultaten ({{responseTime.toFixed(2)}} seconden)
</div>
<div class="body__inner">
<div class="body__facets">
<div v-for="facet in facets.categories" v-bind:key="facet.category"
v-bind:class="{active: activeCategories.includes(facet.category)}"
class="category facet" @click="toggleCategory(facet)">
<div class="category__facet">{{facet.category}}</div>
<div v-if="facet.count < 10000" class="category__count">{{facet.count}}</div>
<div v-else class="category__count">{{(facet.count / 1000).toFixed()}}K</div>
</div>
</div>
<div class="body__results">
<div v-for="result in results" v-bind:key="result.id" class="result">
<a :href="result.url" target="_blank">
<div class="result__title">{{result.title}}</div>
<div class="result__link">{{result.url}}</div>
<div class="result__body">
<span class="result__date">{{result.date}} -</span>
{{result.body}}
</div>
</a>
</div>
</div>
</div>
</div>
<top-bar v-model="query" />
<result-body :query="query" />
</div>
</template>
<style scoped>
a {
text-decoration: none;
}
.topbar {
background-color: rgb(250, 250, 250);
display: flex;
flex-flow: row;
align-items: center;
}
.logo__image {
width: 180px;
margin: 20px 16px 12px;
}
.query__input {
width: 550px;
margin: 16px;
}
.body {
width: 100%;
}
.body__inner {
display: flex;
}
.result-count {
line-height: 43px;
color: rgba(0, 0, 0, 0.5);
margin-left: 208px;
}
.result {
margin: 24px 0;
max-width: 600px;
}
.result:first-child {
margin-top: 12px;
}
.result__title {
font-size: 18px;
margin: 1px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.result__link {
font-size: 14px;
color: #006621;
}
.result__body {
margin: 4px 0;
color: rgba(0, 0, 0, .68);
}
.result__date {
color: rgba(0, 0, 0, .5);
}
.body__facets {
width: 184px;
margin-right: 24px;
}
.facet {
padding: 4px 8px;
cursor: pointer;
}
.facet:hover {
background-color: rgba(0, 0, 0, .1);
}
.facet.active {
font-weight: bold;
}
.category {
display: flex;
justify-content: space-between;
}
.category__facet {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 8px;
}
.category__count {
text-align: right;
}
</style>
<script>
import Vue from "vue";
import {Component} from "vue-property-decorator";
@Component
import ResultBody from "@/components/ResultBody";
import TopBar from "@/components/TopBar";
@Component({
"components": {ResultBody, TopBar},
})
export default class Results extends Vue {
query = "";
hits = 0;
results = [];
facets = {};
activeCategories = [];
responseTime = 0.0;
async toggleCategory(category) {
const name = category.category;
const index = this.activeCategories.indexOf(name);
if (index === -1) {
this.activeCategories.push(name);
} else {
this.activeCategories.splice(index, 1);
}
await this.getResults();
}
async getResults() {
let url = `/api/?q=${this.query}`;
if (this.activeCategories.length > 0) {
const categories = encodeURIComponent(this.activeCategories.join(","));
url += `&categories=${categories}`;
}
let response = await fetch(url);
let json = await response.json();
this.results = json.results;
this.hits = json.hits;
this.responseTime = json.took;
this.facets = json.facets;
}
async mounted() {
await this.getResults();
this.query = this.$route.query.q;
}
}
</script>

View File

@ -0,0 +1,44 @@
<template>
<div class="topbar">
<router-link class="logo" to="/">
<img class="logo__image" src="@/assets/logo.png">
</router-link>
<div class="query">
<input class="query__input" @keyup.enter="search" :value="value">
</div>
</div>
</template>
<style scoped>
.topbar {
background-color: rgb(250, 250, 250);
display: flex;
flex-flow: row;
align-items: center;
}
.logo__image {
width: 180px;
margin: 20px 16px 12px;
}
.query__input {
width: 550px;
margin: 16px;
}
</style>
<script>
import Vue from "vue";
import {Component, Prop} from "vue-property-decorator";
@Component
export default class TopBar extends Vue {
@Prop() value = "";
search(event) {
this.$emit("input", event.target.value)
}
}
</script>

View File

@ -7,6 +7,7 @@ import Results from "@/components/Results";
Vue.use(Router);
export default new Router({
mode: "history",
routes: [
{
path: "/search",