Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/IvanchoDev89/maleku-system/llms.txt

Use this file to discover all available pages before exploring further.

Maleku System provides two complementary discovery surfaces: a full-text search engine built on PostgreSQL’s tsvector / tsquery system for keyword-driven queries, and a map search endpoint that returns geo-located property and tour data for interactive map rendering. Both surfaces are powered by the same FastAPI router (app/api/v1/search.py) and SearchService class, rate-limited via SlowAPI, and available without authentication for public browsing.

Search Endpoint

The global search endpoint scans properties, tours, destinations, and blog posts in parallel using ILIKE pattern matching on names and titles.
GET /api/v1/search/?q={query}&limit={n}
ParameterTypeDescription
qstring (required)Search query, 1–200 characters. Sanitised and stripped before use.
limitintegerMaximum results per category (default 10, max 50).
The response groups results by content type:
{
  "properties": [{ "id": "...", "name": "...", "slug": "...", "type": "property", "image": "..." }],
  "tours":      [{ "id": "...", "name": "...", "slug": "...", "type": "tour",     "image": "..." }],
  "destinations": [...],
  "blog":         [...]
}
For property-specific full-text search with ranking, filters, and pagination use:
GET /api/v1/search/properties?q={query}&property_type=hotel&category=beach&region=Guanacaste&min_price=50&max_price=300&limit=20
ParameterTypeDescription
qstring (required)Full-text search query
property_typestringFilter by PropertyType enum value (e.g. hotel, villa, eco_lodge)
categorystringFilter by PropertyCategory (e.g. beach, mountain, jungle)
regionstringPartial-match filter on the property’s region field
min_pricefloatMinimum base_price
max_pricefloatMaximum base_price
limitintegerMax results (default 20, max 100)
The SearchService class uses PostgreSQL’s native full-text search functions rather than LIKE queries, giving relevance-ranked results with language-aware stemming. How it works:
  1. The search query is converted to a tsquery via plainto_tsquery('spanish', query), which tokenises, stems, and handles phrase variants for Spanish-language content.
  2. A tsvector is computed on-the-fly by concatenating the searchable columns (name, description, city, region for properties; name, description, location for tours; title, content, excerpt for blog posts).
  3. The @@ operator checks for matches, and ts_rank(tsvector, tsquery) produces a relevance score used for ORDER BY rank DESC.
# From search_service.py (property search)
tsquery = func.plainto_tsquery("spanish", query)
tsvector = func.to_tsvector(
    "spanish",
    func.coalesce(Property.name, "")
    + " " + func.coalesce(Property.short_description, "")
    + " " + func.coalesce(Property.description, "")
    + " " + func.coalesce(Property.city, "")
    + " " + func.coalesce(Property.region, ""),
)
stmt = select(Property, func.ts_rank(tsvector, tsquery).label("rank")).where(
    tsvector.op("@@")(tsquery),
    Property.deleted_at.is_(None),
    Property.is_active,
)
GIN Indexes are defined on Property, Tour, and BlogPost models to make these tsvector queries efficient at scale:
# From models/property.py
Index(
    "idx_property_fts",
    sa.text(
        "to_tsvector('spanish', COALESCE(name, '') || ' ' || "
        "COALESCE(short_description, '') || ' ' || "
        "COALESCE(description, '') || ' ' || "
        "COALESCE(city, '') || ' ' || COALESCE(region, ''))"
    ),
    postgresql_using="gin",
)
Similar idx_tour_fts and blog GIN indexes are defined on their respective models. The escape_like_pattern() utility from app/core/utils.py is used for all ILIKE queries throughout the codebase (tours list endpoint, global search, vehicle/boat search) to prevent SQL injection via unescaped % and _ characters. The map endpoint returns all geo-located properties and tours in a single paginated response optimised for rendering map pins.
GET /api/v1/search/map?property_type=&category=&region=&min_price=&max_price=&page=1&page_size=50
ParameterTypeDescription
property_typestringOptional filter by property type
categorystringOptional filter by property or tour category
regionstringOptional filter by region
min_price / max_pricefloatPrice range filter
pageintegerPage number (default 1)
page_sizeintegerResults per page (default 50, max 200)
Only properties with non-null latitude and longitude are included in the map results. Tours are included regardless of coordinates (they use the location text field for display). A companion endpoint GET /api/v1/search/map/count returns aggregate counts grouped by region and tour category, useful for rendering cluster badges on the map.
The Nuxt.js 3 frontend renders these map API responses using Leaflet.js (v1.9.4). Property and tour items from the /search/map endpoint are mapped directly to Leaflet markers. The latitude/longitude fields on MapPropertyItem drive pin placement, while cover_image, base_price, and rating populate the marker popup cards.

Search Result Schemas

MapPropertyItem

Returned by GET /api/v1/search/map for each property.
FieldTypeDescription
idstringProperty UUID
namestringProperty display name
slugstringURL-friendly identifier
property_typestringe.g. hotel, eco_lodge, villa
categorystring | nullLocation category (e.g. beach, mountain)
region / citystring | nullGeographic location fields
addressstring | nullFull street address
latitude / longitudefloat | nullMap coordinates
cover_imagestring | nullPrimary image URL
imagesstring[]Up to 4 image URLs
base_pricefloatStarting nightly rate
weekend_pricefloatWeekend nightly rate
ratingfloat | nullAverage review rating (0–5)
total_reviewsint | nullNumber of reviews
min_guests / max_guestsintMinimum and maximum guest capacity
amenitiesstring[]Up to 8 amenity tags

MapTourItem

FieldTypeDescription
idstringTour UUID
namestringTour display name
slugstringURL-friendly identifier
categorystring | nulle.g. adventure, nature, water
difficultystring | nulleasy, medium, or hard
locationstring | nullText location / region
latitude / longitudefloat | nullCoordinates (currently null; resolved from location)
cover_imagestring | nullPrimary image URL
imagesstring[]Up to 4 image URLs
pricefloatPer-person price
duration_hoursfloatTour duration in hours
ratingfloat | nullAverage review rating
max_group_sizeintMaximum participants per booking

Full-Text TextSearchItem

Returned by GET /api/v1/search/properties.
FieldTypeDescription
idstringProperty UUID
namestringProperty name
slugstringURL slug
property_type / categorystring | nullType and category
region / citystring | nullLocation
cover_imagestring | nullPrimary image URL
base_pricefloatStarting nightly rate
ratingfloat | nullAverage rating
total_reviewsint | nullReview count
match_typestringAlways "text" for full-text search results
rankfloatPostgreSQL ts_rank relevance score

Tour-Specific Filters

The GET /api/v1/tours/ listing endpoint supports a richer filter set specifically for tour discovery. These parameters were explicitly added and validated in the project changelog:
ParameterTypeDescription
qstringText search across name and description using ILIKE
destinationstringAlias for location; partial match on the tour’s location field
min_durationfloatMinimum duration_hours
max_durationfloatMaximum duration_hours
min_ratingfloatMinimum average rating (alias for rating)
sortstringSort order: popular, price_asc, price_desc, rating, newest
Both rating and min_rating resolve to the same filter (Tour.rating >= value); the API accepts either parameter name for compatibility.

Rate Limiting

All search endpoints are rate-limited using SlowAPI (limiter from app/core/rate_limiter.py). The default limit is 30 requests per minute per IP address:
@router.get("/properties")
@limiter.limit("30/minute")
async def search_properties(request: Request, ...):
    ...
The same 30 req/min limit applies to GET /api/v1/search/map, GET /api/v1/search/map/count, and GET /api/v1/search/. Requests exceeding the limit receive a 429 Too Many Requests response. The price preview endpoint (POST /api/v1/bookings/preview) uses a separate limit of 30 req/min. Other endpoints across the platform use the global default of 60 req/min.

Example Search Request

# Full-text property search with filters
curl -G "http://localhost:8000/api/v1/search/properties" \
  --data-urlencode "q=jungle lodge" \
  --data-urlencode "category=jungle" \
  --data-urlencode "region=Osa" \
  --data-urlencode "min_price=80" \
  --data-urlencode "max_price=400" \
  --data-urlencode "limit=10"
# Map data for Guanacaste, beach properties only
curl "http://localhost:8000/api/v1/search/map?region=Guanacaste&category=beach&page=1&page_size=50"
# Global search across all content types
curl -G "http://localhost:8000/api/v1/search/" \
  --data-urlencode "q=Manuel Antonio" \
  --data-urlencode "limit=5"

Build docs developers (and LLMs) love