Skip to content

Commit

Permalink
add sqlite search, cleanup and ci
Browse files Browse the repository at this point in the history
  • Loading branch information
tofran committed Mar 31, 2024
1 parent 3cc7f04 commit 76cf9ef
Show file tree
Hide file tree
Showing 18 changed files with 282 additions and 107 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Lint

on:
push:
pull_request:

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'

- run: make install-dev

- run: make lint
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
venv
/static/*.min.js
.env
sqlite
23 changes: 0 additions & 23 deletions labels.html

This file was deleted.

Binary file added products.sqlite
Binary file not shown.
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
pyright==1.1.355
pyright==1.1.356
ruff==0.3.4
8 changes: 2 additions & 6 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ ol {

form {
margin-bottom: 1rem;
line-height: 3rem;
}

input[type="text"] {
Expand All @@ -83,11 +84,6 @@ input[type="text"]:focus {
border: 1px solid #555;
}

.algolia-logo {
width: 20px;
vertical-align: baseline;
}

label {
color: #E0E0E0;
margin-right: 10px;
Expand All @@ -99,7 +95,7 @@ select {
border: none;
padding: 10px;
border-radius: var(--border-radius);
min-width: min(20rem, calc(100% - var(--padding-body)));
/* min-width: min(20rem, calc(100% - var(--padding-body))); */
border: 1px solid transparent;
}

Expand Down
28 changes: 17 additions & 11 deletions templates/index.jinja.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,27 @@ <h1>Text search example</h1>

<form
hx-get="/html-api/items"
hx-params="search_query"
hx-params="search_query, search_index"
hx-target="#search-results-container"
hx-trigger="keyup delay:800ms"
>
<input type="text" name="search_query" placeholder="Search...">
hx-trigger="keyup delay:800ms from:input queue:last changed, change from:select queue:last"
>
<input
type="text"
name="search_query"
placeholder="Search..."
/>

<!-- TODO: add more search providers -->
<!-- <label for="index">Search with:</label>
<select id="index" name="index">
<option value="algolia">Algolia</option>
<option value="sqlite">SQLite</option>
</select> -->
<label for="search-index-select">Search with:</label>
<select
id="search-index-select"
name="search_index"
hx-trigger="change"
>
<option value="SQLITE">SQLite</option>
<option value="ALGOLIA">Algolia</option>
</select>
</form>


<div id="search-results-container">
{% include "results.jinja.html" %}
</div>
Expand Down
23 changes: 20 additions & 3 deletions text_search_app/app.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.requests import Request
from fastapi.staticfiles import StaticFiles
from starlette.exceptions import HTTPException as StarletteHTTPException

from text_search_app.config import DEVELOPMENT_MODE
from text_search_app.routers import html_api_router, index_management_router, page_router
from text_search_app.routers import (
html_api_router,
index_management_router,
misc_router,
page_router,
)
from text_search_app.search_indexes import sqlite_index
from text_search_app.templates import make_template_response


@asynccontextmanager
async def lifespan(app: FastAPI):
sqlite_index.setup_db()
yield


app = FastAPI(
title="Search sample",
title="Python FastAPI HTMX full-text-search demo",
debug=DEVELOPMENT_MODE,
openapi_url="/openapi.json" if DEVELOPMENT_MODE else None,
lifespan=lifespan,
)


app.include_router(page_router.router)
app.include_router(index_management_router.router)
app.include_router(html_api_router.router)
app.include_router(index_management_router.router)
app.include_router(misc_router.router)


app.mount(
Expand Down
4 changes: 4 additions & 0 deletions text_search_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@
ALGOLIA_APP_ID = os.environ["ALGOLIA_APP_ID"]
ALGOLIA_API_KEY = os.environ["ALGOLIA_API_KEY"]
ALGOLIA_INDEX_NAME = os.environ["ALGOLIA_INDEX_NAME"]

SQLITE_DATABASE_PATH = "products.sqlite"

RESULTS_PAGE_SIZE = 8
2 changes: 1 addition & 1 deletion text_search_app/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s] [%(levelname)s] %(pathname)s:%(lineno)d | %(message)s",
format="[%(asctime)s] [%(levelname)s] %(message)s (%(pathname)s:%(lineno)d)",
)

logger = logging.getLogger(text_search_app.__name__)
12 changes: 3 additions & 9 deletions text_search_app/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, field_validator


class Product(BaseModel):
Expand All @@ -7,8 +7,8 @@ class Product(BaseModel):
description: str
price: float
currency: str
terms: str
section: str
terms: str | None
section: str | None

@field_validator("price", mode="before")
def transform_str_to_float(
Expand All @@ -19,9 +19,3 @@ def transform_str_to_float(
return float(value.replace(",", "."))

return value


class AlgoliaSearchResult(Product):
object_id: str = Field(
alias="objectId",
)
16 changes: 13 additions & 3 deletions text_search_app/routers/html_api_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from fastapi.responses import HTMLResponse
from starlette.exceptions import HTTPException as StarletteHTTPException

from text_search_app.search_indexes.algolia import get_items
from text_search_app.config import DEVELOPMENT_MODE
from text_search_app.search_indexes.indexes import SearchIndex, get_items
from text_search_app.templates import make_template_response


Expand All @@ -21,7 +21,9 @@ async def prevent_html_api_direct_access(
return

# Simulate a 404 just for demonstration - I'm avoiding custom errors in this project
raise StarletteHTTPException(status_code=status.HTTP_404_NOT_FOUND)
raise StarletteHTTPException(
status_code=status.HTTP_404_NOT_FOUND,
)


router = APIRouter(
Expand All @@ -37,14 +39,22 @@ async def prevent_html_api_direct_access(
)
async def get_items_html(
request: Request,
search_index: SearchIndex = Query(
default=SearchIndex.SQLITE,
description="The search index to use",
),
search_query: str = Query(
default="",
description="The user query to find products that better match it",
),
):
return make_template_response(
"results",
request,
{
"items": get_items(search_query),
"items": get_items(
search_index=search_index,
search_query=search_query,
)
},
)
4 changes: 2 additions & 2 deletions text_search_app/routers/index_management_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from fastapi import APIRouter, File, UploadFile
from fastapi.responses import PlainTextResponse

from text_search_app.search_indexes.algolia import load_products_into_index
from text_search_app.logging import logger
from text_search_app.models import Product
from text_search_app.search_indexes import indexes

router = APIRouter(
prefix="/index",
Expand Down Expand Up @@ -36,7 +36,7 @@ async def upload_products(
for row in reader
]

load_products_into_index(products)
indexes.load_products_into_index(products)

return PlainTextResponse(
content=f"Successfully indexed {len(products)} products.",
Expand Down
31 changes: 31 additions & 0 deletions text_search_app/routers/misc_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from typing import Literal

from fastapi import APIRouter, Response
from fastapi.responses import PlainTextResponse

router = APIRouter(
prefix="",
tags=["Miscellaneous"],
)


@router.get(
"/healthz",
response_model=Literal["OK"],
)
async def health_check():
return Response(
"OK",
media_type="text/plain",
)


@router.get(
"/robots.txt",
response_class=PlainTextResponse,
)
async def get_robots_txt():
return PlainTextResponse(
"User-agent: *\nDisallow: /html-api",
media_type="text/plain",
)
39 changes: 7 additions & 32 deletions text_search_app/routers/page_router.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,30 @@
from typing import Literal

from fastapi import APIRouter, Query, Response
from fastapi import APIRouter
from fastapi.requests import Request
from fastapi.responses import HTMLResponse

from text_search_app.search_indexes.algolia import get_items
from text_search_app.search_indexes import indexes
from text_search_app.templates import make_template_response

router = APIRouter(
prefix="",
tags=["Front-end HTML pages"],
)


@router.get(
"/healthz",
response_model=Literal["OK"],
tags=["Others"],
)
async def health_check():
return Response(
"OK",
media_type="text/plain",
)


@router.get(
"/robots.txt",
tags=["Others"],
)
async def get_robots_txt():
return Response(
"User-agent: *\nDisallow: /html-api",
media_type="text/plain",
)


@router.get(
"/",
response_class=HTMLResponse,
tags=["Front-end HTML pages"],
)
async def get_index_html(
request: Request,
search_query: str = Query(
default="",
),
# TODO: later one could add a search_query query params to preserve the user state
):
return make_template_response(
"index",
request,
{
"items": get_items(search_query),
"items": indexes.get_items(
search_query=None,
),
},
)

0 comments on commit 76cf9ef

Please sign in to comment.