mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-13 03:13:12 -05:00
feat: Generalize Search to Other Models (#2472)
* generalized search logic to SearchFilter * added default search behavior for all models * fix for schema overrides * added search support to several models * fix for label search * tests and fixes * add config for normalizing characters * dramatically simplified search tests * bark bark * fix normalization bug * tweaked tests * maybe this time? --------- Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
@@ -2,16 +2,17 @@ from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, ClassVar
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import UUID4, BaseModel, Field, validator
|
||||
from slugify import slugify
|
||||
from sqlalchemy.orm import joinedload, selectinload
|
||||
from sqlalchemy import Select, desc, func, or_, select, text
|
||||
from sqlalchemy.orm import Session, joinedload, selectinload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
|
||||
from mealie.core.config import get_app_dirs
|
||||
from mealie.schema._mealie import MealieModel
|
||||
from mealie.schema._mealie import MealieModel, SearchType
|
||||
from mealie.schema.response.pagination import PaginationBase
|
||||
|
||||
from ...db.models.recipe import (
|
||||
@@ -37,6 +38,8 @@ class RecipeTag(MealieModel):
|
||||
name: str
|
||||
slug: str
|
||||
|
||||
_searchable_properties: ClassVar[list[str]] = ["name"]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@@ -78,6 +81,7 @@ class CreateRecipe(MealieModel):
|
||||
|
||||
class RecipeSummary(MealieModel):
|
||||
id: UUID4 | None
|
||||
_normalize_search: ClassVar[bool] = True
|
||||
|
||||
user_id: UUID4 = Field(default_factory=uuid4)
|
||||
group_id: UUID4 = Field(default_factory=uuid4)
|
||||
@@ -259,6 +263,69 @@ class Recipe(RecipeSummary):
|
||||
selectinload(RecipeModel.notes),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def filter_search_query(
|
||||
cls, db_model, query: Select, session: Session, search_type: SearchType, search: str, search_list: list[str]
|
||||
) -> Select:
|
||||
"""
|
||||
1. token search looks for any individual exact hit in name, description, and ingredients
|
||||
2. fuzzy search looks for trigram hits in name, description, and ingredients
|
||||
3. Sort order is determined by closeness to the recipe name
|
||||
Should search also look at tags?
|
||||
"""
|
||||
|
||||
if search_type is SearchType.fuzzy:
|
||||
# I would prefer to just do this in the recipe_ingredient.any part of the main query,
|
||||
# but it turns out that at least sqlite wont use indexes for that correctly anymore and
|
||||
# takes a big hit, so prefiltering it is
|
||||
ingredient_ids = (
|
||||
session.execute(
|
||||
select(RecipeIngredientModel.id).filter(
|
||||
or_(
|
||||
RecipeIngredientModel.note_normalized.op("%>")(search),
|
||||
RecipeIngredientModel.original_text_normalized.op("%>")(search),
|
||||
)
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
session.execute(text(f"set pg_trgm.word_similarity_threshold = {cls._fuzzy_similarity_threshold};"))
|
||||
return query.filter(
|
||||
or_(
|
||||
RecipeModel.name_normalized.op("%>")(search),
|
||||
RecipeModel.description_normalized.op("%>")(search),
|
||||
RecipeModel.recipe_ingredient.any(RecipeIngredientModel.id.in_(ingredient_ids)),
|
||||
)
|
||||
).order_by( # trigram ordering could be too slow on million record db, but is fine with thousands.
|
||||
func.least(
|
||||
RecipeModel.name_normalized.op("<->>")(search),
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
ingredient_ids = (
|
||||
session.execute(
|
||||
select(RecipeIngredientModel.id).filter(
|
||||
or_(
|
||||
*[RecipeIngredientModel.note_normalized.like(f"%{ns}%") for ns in search_list],
|
||||
*[RecipeIngredientModel.original_text_normalized.like(f"%{ns}%") for ns in search_list],
|
||||
)
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
return query.filter(
|
||||
or_(
|
||||
*[RecipeModel.name_normalized.like(f"%{ns}%") for ns in search_list],
|
||||
*[RecipeModel.description_normalized.like(f"%{ns}%") for ns in search_list],
|
||||
RecipeModel.recipe_ingredient.any(RecipeIngredientModel.id.in_(ingredient_ids)),
|
||||
)
|
||||
).order_by(desc(RecipeModel.name_normalized.like(f"%{search}%")))
|
||||
|
||||
|
||||
class RecipeLastMade(BaseModel):
|
||||
timestamp: datetime.datetime
|
||||
|
||||
Reference in New Issue
Block a user