Skip to content

Commit 10b968f

Browse files
Search tutorial files (elastic#108)
* tutorial support files * updates to the code * relocate under example-apps * v2 with full-text and vector search * v3 with elser search
1 parent a4a9671 commit 10b968f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1901
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Search Tutorial
2+
3+
This directory contains support files used in the Elasticsearch [Search Tutorial](https://www.elastic.co/search-labs/guides/search-tutorial/welcome).
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/sh
2+
cd start
3+
zip -r ../search-tutorial-starter.zip \
4+
search-tutorial/README.md \
5+
search-tutorial/LICENSE \
6+
search-tutorial/requirements.txt \
7+
search-tutorial/data.json \
8+
search-tutorial/.flaskenv \
9+
search-tutorial/app.py \
10+
search-tutorial/templates/* \
11+
search-tutorial/static/*
Binary file not shown.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FLASK_DEBUG=1
2+
FLASK_RUN_PORT=5001
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2023 Elasticsearch B.V.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of
6+
this software and associated documentation files (the "Software"), to deal in
7+
the Software without restriction, including without limitation the rights to
8+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9+
the Software, and to permit persons to whom the Software is furnished to do so,
10+
subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Elasticsearch Search Tutorial
2+
3+
This directory contains a starter Flask project used in the Search tutorial.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import re
2+
from flask import Flask, render_template, request
3+
4+
app = Flask(__name__)
5+
6+
7+
@app.get('/')
8+
def index():
9+
return render_template('index.html')
10+
11+
12+
@app.post('/')
13+
def handle_search():
14+
query = request.form.get('query', '')
15+
return render_template(
16+
'index.html', query=query, results=[], from_=0, total=0)
17+
18+
19+
@app.get('/document/<id>')
20+
def get_document(id):
21+
return 'Document not found'

example-apps/search-tutorial/start/search-tutorial/data.json

Lines changed: 145 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pip-tools
2+
flask
3+
python-dotenv
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.11
3+
# by the following command:
4+
#
5+
# pip-compile requirements.in
6+
#
7+
blinker==1.6.3
8+
# via flask
9+
build==1.0.3
10+
# via pip-tools
11+
click==8.1.7
12+
# via
13+
# flask
14+
# pip-tools
15+
flask==3.0.0
16+
# via -r requirements.in
17+
itsdangerous==2.1.2
18+
# via flask
19+
jinja2==3.1.2
20+
# via flask
21+
markupsafe==2.1.3
22+
# via
23+
# jinja2
24+
# werkzeug
25+
packaging==23.2
26+
# via build
27+
pip-tools==7.3.0
28+
# via -r requirements.in
29+
pyproject-hooks==1.0.0
30+
# via build
31+
python-dotenv==1.0.0
32+
# via -r requirements.in
33+
werkzeug==3.0.1
34+
# via flask
35+
wheel==0.41.3
36+
# via pip-tools
37+
38+
# The following packages are considered to be unsafe in a requirements file:
39+
# pip
40+
# setuptools
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Elasticsearch Tutorial</title>
7+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
8+
</head>
9+
<body>
10+
<div class="container">
11+
<nav class="navbar bg-dark border-bottom border-body mb-3" data-bs-theme="dark">
12+
<div class="container-fluid">
13+
<a class="navbar-brand" href="#">
14+
<img src="{{ url_for('static', filename='elastic-logo.svg') }}" alt="Elastic" width="30" height="24" class="d-inline-block align-text-top">
15+
Elasticsearch Tutorial
16+
</a>
17+
</div>
18+
</nav>
19+
{% block content %}{% endblock %}
20+
</div>
21+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
22+
</body>
23+
</html>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{% extends "base.html" %}
2+
3+
{% block content %}
4+
<p><a href="javascript:history.back(1)" class="btn btn-primary">← Back</a></p>
5+
<h2>{{ title }}</h2>
6+
{% for paragraph in paragraphs %}
7+
<p>{{ paragraph }}</p>
8+
{% endfor %}
9+
{% endblock %}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{% extends 'base.html' %}
2+
3+
{% block content %}
4+
<form method="POST" action="{{ url_for('handle_search') }}">
5+
<div class="mb-3">
6+
<input type="text" class="form-control" name="query" id="query" placeholder="Enter your search query" autofocus>
7+
</div>
8+
</form>
9+
{% if results %}
10+
<div class="row mb-3">
11+
<div class="col-sm-auto my-auto">
12+
Showing results {{ from_ + 1 }}-{{ from_ + results|length }} out of {{ total }}.
13+
</div>
14+
{% if from_ > 0 %}
15+
<div class="col-sm-auto my-auto">
16+
<a href="javascript:history.back(1)" class="btn btn-primary">← Previous page</a>
17+
</div>
18+
{% endif %}
19+
{% if from_ + results|length < total %}
20+
<div class="col-sm-auto my-auto">
21+
<form method="POST">
22+
<input type="hidden" name="query" value="{{ query }}">
23+
<input type="hidden" name="from_" value="{{ from_ + results|length }}">
24+
<button type="submit" class="btn btn-primary">Next page →</button>
25+
</form>
26+
</div>
27+
{% endif %}
28+
<div class="col"></div>
29+
</div>
30+
{% for result in results %}
31+
<p>
32+
{{ from_ + loop.index }}. <b><a href="{{ url_for('get_document', id=result._id) }}">{{ result._source.name }}</a></b>
33+
<br>
34+
{{ result._source.summary }}
35+
<br>
36+
<small>
37+
Category: {{ result._source.category }}.
38+
Last updated: {{ result._source.updated_at | default(result._source.created_on) }}.
39+
{% if result._score %}<i>(Score: {{ result._score }})</i>{% endif %}
40+
</small>
41+
</p>
42+
{% endfor %}
43+
{% elif request.method == 'POST' %}
44+
<p>No results found.</p>
45+
{% endif %}
46+
{% endblock %}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FLASK_DEBUG=1
2+
FLASK_RUN_PORT=5001
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2023 Elasticsearch B.V.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of
6+
this software and associated documentation files (the "Software"), to deal in
7+
the Software without restriction, including without limitation the rights to
8+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9+
the Software, and to permit persons to whom the Software is furnished to do so,
10+
subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Elasticsearch Search Tutorial
2+
3+
This directory contains the Search Tutorial application with full-text search functionality.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import re
2+
from flask import Flask, render_template, request
3+
from search import Search
4+
5+
app = Flask(__name__)
6+
es = Search()
7+
8+
9+
@app.get('/')
10+
def index():
11+
return render_template('index.html')
12+
13+
14+
@app.post('/')
15+
def handle_search():
16+
query = request.form.get('query', '')
17+
filters, parsed_query = extract_filters(query)
18+
from_ = request.form.get('from_', type=int, default=0)
19+
20+
if parsed_query:
21+
search_query = {
22+
'must': {
23+
'multi_match': {
24+
'query': parsed_query,
25+
'fields': ['name', 'summary', 'content'],
26+
}
27+
}
28+
}
29+
else:
30+
search_query = {
31+
'must': {
32+
'match_all': {}
33+
}
34+
}
35+
36+
results = es.search(
37+
query={
38+
'bool': {
39+
**search_query,
40+
**filters
41+
}
42+
}, size=5, from_=from_
43+
)
44+
return render_template('index.html', results=results['hits']['hits'],
45+
query=query, from_=from_,
46+
total=results['hits']['total']['value'])
47+
48+
49+
@app.get('/document/<id>')
50+
def get_document(id):
51+
document = es.retrieve_document(id)
52+
title = document['_source']['name']
53+
paragraphs = document['_source']['content'].split('\n')
54+
return render_template('document.html', title=title, paragraphs=paragraphs)
55+
56+
57+
@app.cli.command()
58+
def reindex():
59+
"""Regenerate the Elasticsearch index."""
60+
response = es.reindex()
61+
print(f'Index with {len(response["items"])} documents created '
62+
f'in {response["took"]} milliseconds.')
63+
64+
65+
def extract_filters(query):
66+
filter_regex = r'category:([^\s]+)\s*'
67+
m = re.search(filter_regex, query)
68+
if m is None:
69+
return {}, query # no filters
70+
filters = {
71+
'filter': [{
72+
'term': {
73+
'category.keyword': {
74+
'value': m.group(1)
75+
}
76+
}
77+
}]
78+
}
79+
query = re.sub(filter_regex, '', query).strip()
80+
return filters, query
81+

0 commit comments

Comments
 (0)