Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions examples/book-search/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->

# Book Search — Vespa Testcontainers demo

A simple book search application demonstrating how to use [vespa-testcontainers](https://github.com/vespaai/vespa-testcontainers) for integration testing.

## Running the app locally

**1. Start Vespa**
```sh
docker run --detach --name vespa --hostname vespa-container \
--publish 8080:8080 --publish 19071:19071 \
vespaengine/vespa
```

**2. Deploy the application package**
```sh
vespa deploy app --wait 60
```

**3. Run the frontend**
```sh
cd frontend
uv run streamlit run app.py
```

Open the app at http://localhost:8501 and click **Feed documents** to load the dataset.

## Running the tests

The tests use `VespaContainer` to spin up an isolated Vespa instance automatically — no manual setup needed.

First, install `vespa-testcontainers` to your local Maven repository:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not be necessary after a proper publishing pipeline has been set up I guess (ref. this )

```sh
cd /path/to/vespa-testcontainers
./gradlew publishToMavenLocal
```

Then run the tests:
```sh
mvn test
```

> [!NOTE]
> When using Podman instead of Docker, you have to set
> ```bash
> export DOCKER_HOST="unix://"$(podman machine inspect --format {{.ConnectionInfo.PodmanSocket.Path}})
> export TESTCONTAINERS_RYUK_DISABLED=true
> ```
39 changes: 39 additions & 0 deletions examples/book-search/app/schemas/book.sd
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
schema book {

document book {

field title type string {
indexing: summary | index
index: enable-bm25
}

field author type string {
indexing: summary | index
index: enable-bm25
}

field year type int {
indexing: summary | attribute
}

field themes type array<string> {
indexing: summary | attribute | index
}

field loaned_out type bool {
indexing: summary | attribute
}

}

fieldset default {
fields: title, author
}

rank-profile bm25 inherits default {
first-phase {
expression: bm25(title) + bm25(author)
}
}

}
22 changes: 22 additions & 0 deletions examples/book-search/app/services.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8" ?>
<services version="1.0">

<container id="default" version="1.0">
<document-api/>
<search/>
<nodes>
<node hostalias="node1" />
</nodes>
</container>

<content id="book" version="1.0">
<min-redundancy>1</min-redundancy>
<documents>
<document type="book" mode="index" />
</documents>
<nodes>
<node hostalias="node1" distribution-key="0" />
</nodes>
</content>

</services>
20 changes: 20 additions & 0 deletions examples/book-search/data/documents.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{"put": "id:library:book::the-lord-of-the-rings", "fields": {"title": "The Lord of the Rings", "author": "J.R.R. Tolkien", "year": 1954, "themes": ["fantasy", "adventure"], "loaned_out": false}}
{"put": "id:library:book::nineteen-eighty-four", "fields": {"title": "Nineteen Eighty-Four", "author": "George Orwell", "year": 1949, "themes": ["dystopia", "political"], "loaned_out": false}}
{"put": "id:library:book::to-kill-a-mockingbird", "fields": {"title": "To Kill a Mockingbird", "author": "Harper Lee", "year": 1960, "themes": ["classic", "social"], "loaned_out": false}}
{"put": "id:library:book::the-great-gatsby", "fields": {"title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "year": 1925, "themes": ["classic", "tragedy"], "loaned_out": false}}
{"put": "id:library:book::brave-new-world", "fields": {"title": "Brave New World", "author": "Aldous Huxley", "year": 1932, "themes": ["dystopia", "sci-fi"], "loaned_out": false}}
{"put": "id:library:book::the-hitchhikers-guide", "fields": {"title": "The Hitchhiker's Guide to the Galaxy", "author": "Douglas Adams", "year": 1979, "themes": ["sci-fi", "comedy"], "loaned_out": false}}
{"put": "id:library:book::dune", "fields": {"title": "Dune", "author": "Frank Herbert", "year": 1965, "themes": ["sci-fi", "adventure"], "loaned_out": false}}
{"put": "id:library:book::the-catcher-in-the-rye", "fields": {"title": "The Catcher in the Rye", "author": "J.D. Salinger", "year": 1951, "themes": ["classic", "coming-of-age"], "loaned_out": false}}
{"put": "id:library:book::harry-potter-philosophers-stone", "fields": {"title": "Harry Potter and the Philosopher's Stone", "author": "J.K. Rowling", "year": 1997, "themes": ["fantasy", "young-adult"], "loaned_out": false}}
{"put": "id:library:book::crime-and-punishment", "fields": {"title": "Crime and Punishment", "author": "Fyodor Dostoevsky", "year": 1866, "themes": ["classic", "psychological"], "loaned_out": false}}
{"put": "id:library:book::fahrenheit-451", "fields": {"title": "Fahrenheit 451", "author": "Ray Bradbury", "year": 1953, "themes": ["dystopia", "sci-fi"], "loaned_out": false}}
{"put": "id:library:book::the-name-of-the-wind", "fields": {"title": "The Name of the Wind", "author": "Patrick Rothfuss", "year": 2007, "themes": ["fantasy", "adventure"], "loaned_out": false}}
{"put": "id:library:book::a-study-in-scarlet", "fields": {"title": "A Study in Scarlet", "author": "Arthur Conan Doyle", "year": 1887, "themes": ["mystery", "detective"], "loaned_out": false}}
{"put": "id:library:book::moby-dick", "fields": {"title": "Moby-Dick", "author": "Herman Melville", "year": 1851, "themes": ["classic", "adventure"], "loaned_out": false}}
{"put": "id:library:book::neuromancer", "fields": {"title": "Neuromancer", "author": "William Gibson", "year": 1984, "themes": ["sci-fi", "cyberpunk"], "loaned_out": false}}
{"put": "id:library:book::foundation", "fields": {"title": "Foundation", "author": "Isaac Asimov", "year": 1951, "themes": ["sci-fi"], "loaned_out": false}}
{"put": "id:library:book::the-handmaids-tale", "fields": {"title": "The Handmaid's Tale", "author": "Margaret Atwood", "year": 1985, "themes": ["dystopia", "political"], "loaned_out": false}}
{"put": "id:library:book::stardust", "fields": {"title": "Stardust", "author": "Neil Gaiman", "year": 1999, "themes": ["fantasy", "adventure"], "loaned_out": false}}
{"put": "id:library:book::the-alchemist", "fields": {"title": "The Alchemist", "author": "Paulo Coelho", "year": 1988, "themes": ["adventure", "philosophy"], "loaned_out": false}}
{"put": "id:library:book::the-old-man-and-the-sea", "fields": {"title": "The Old Man and the Sea", "author": "Ernest Hemingway", "year": 1952, "themes": ["classic", "adventure"], "loaned_out": false}}
86 changes: 86 additions & 0 deletions examples/book-search/frontend/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import json
from pathlib import Path

import streamlit as st
from vespa.application import Vespa

VESPA_URL = "http://localhost"
VESPA_PORT = 8080
SCHEMA = "book"
NAMESPACE = "library"

ALL_THEMES = ["adventure", "classic", "comedy", "coming-of-age", "cyberpunk",
"detective", "dystopia", "fantasy", "mystery", "philosophy",
"political", "psychological", "sci-fi", "social", "tragedy", "young-adult"]

app = Vespa(url=VESPA_URL, port=VESPA_PORT)

def feed_catalog():
docs_path = Path(__file__).parent.parent / "data" / "documents.jsonl"
with open(docs_path) as f:
for line in f:
doc = json.loads(line)
doc_id = doc["put"].split("::")[-1]
app.feed_data_point(schema=SCHEMA, data_id=doc_id,
fields=doc["fields"], namespace=NAMESPACE)


def set_loaned_out(doc_id: str, loaned_out: bool):
app.update_data(
schema=SCHEMA,
data_id=doc_id,
fields={"loaned_out": loaned_out},
namespace=NAMESPACE,
)


# Auto-load catalog on first run
check = app.query(yql="select * from book where true", hits=0)
if check.json["root"]["fields"]["totalCount"] == 0:
with st.spinner("Loading library catalog..."):
feed_catalog()

st.title("Library Search")

query = st.text_input("Search by title or author", "")
themes = st.multiselect("Filter by theme", ALL_THEMES)
year_range = st.slider("Publication year", 1800, 2030, (1800, 2030))
only_avail = st.checkbox("Available only")

conditions = [f"year >= {year_range[0]}", f"year <= {year_range[1]}"]
for theme in themes:
conditions.append(f'themes contains "{theme}"')
if only_avail:
conditions.append("loaned_out = false")
if query:
conditions.insert(0, "userInput(@query)")

where = " and ".join(conditions)
params = {"yql": f"select * from book where {where}", "hits": 20}
if query:
params["query"] = query

response = app.query(**params)
hits = response.hits
total = response.json["root"]["fields"]["totalCount"]
st.caption(f"{total} book(s) in library — showing {len(hits)}")

for hit in hits:
f = hit["fields"]
doc_id = hit["id"].split("::")[-1]
on_loan = f.get("loaned_out", False)

col1, col2 = st.columns([4, 1])
with col1:
status = "On loan" if on_loan else "Available"
st.markdown(f"**{f['title']}** — {f['author']} ({f['year']})")
st.caption(f"{status} · {', '.join(f.get('themes', []))}")
with col2:
if on_loan:
if st.button("Return", key=doc_id):
set_loaned_out(doc_id, False)
st.rerun()
else:
if st.button("Loan Out", key=doc_id):
set_loaned_out(doc_id, True)
st.rerun()
8 changes: 8 additions & 0 deletions examples/book-search/frontend/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
name = "book-search-frontend"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"streamlit",
"pyvespa",
]
Loading
Loading