Files
music_library/backlog/completed/ml-176.1 - API-accept-search-query-parameter-on-GET-api-v1-collection.md
T
Claudio Ortolina 8ece3ba891 Backlog cleanup
2026-05-12 11:30:59 +01:00

12 KiB

id, title, status, assignee, created_date, updated_date, labels, milestone, dependencies, references, modified_files, parent_task_id, priority
id title status assignee created_date updated_date labels milestone dependencies references modified_files parent_task_id priority
ML-176.1 API: accept search query parameter on GET /api/v1/collection Done
2026-05-10 13:35 2026-05-11 06:47
api
m-0
lib/music_library/records/search.ex (FTS5 search implementation)
lib/music_library_web/controllers/collection_json.ex (response format)
lib/music_library_web/controllers/collection_controller.ex
ML-176 medium

Description

Extend the existing GET /api/v1/collection endpoint to accept an optional q query parameter for full-text search.

When q is present and non-empty, pass it to Collection.search_records/2 instead of the current empty-string default. When absent or empty, preserve existing behavior (return all collection records).

The existing FTS5 search in Records.Search handles the query — no new search logic is needed. It searches across all indexed fields (title, artists, genres) and supports the artist:, album:, etc. structured syntax, though the Presto client will only send plain text.

Response format: unchanged — the existing CollectionJSON.index/1 already returns {total, limit, offset, records} with all fields the Presto needs.

Key decisions (from user):

  • Collection-only scope (purchased_at IS NOT NULL) — already the default for this endpoint
  • Alphabetical ordering — already the default
  • Plain text query — passed as-is to FTS5
  • Always returns paginated (limit/offset) — existing behavior

Acceptance Criteria

  • #1 GET /api/v1/collection returns all records when q is absent (existing behavior unchanged)
  • #2 GET /api/v1/collection?q=beatles returns only records matching FTS5 query for 'beatles'
  • #3 GET /api/v1/collection?q= returns all records (empty string treated as no query)
  • #4 GET /api/v1/collection?q=beatles&limit=5&offset=0 respects pagination with search
  • #5 Response format matches existing schema: {total, limit, offset, records} with all record fields
  • #6 Controller test covers: no q param, empty q, populated q, q with no matches, q with pagination

Implementation Plan

Implementation Plan

1. Objective alignment

Extend GET /api/v1/collection to accept an optional q query parameter for FTS5 full-text search. When q is present and non-empty, pass it to the existing Collection.search_records/2 and search_records_count/1 pipeline. When absent or empty, preserve the current behavior of returning all collection records. No new search logic is required — Records.Search already handles both empty and populated queries.

2. Simplicity and alternatives considered

Chosen approach: extract q from conn params in the controller, pass it directly.

The controller's index/2 action currently hardcodes "" as the query. Changing it to read params["q"] and normalize empty/absent to "" is a ~5-line change. The entire search pipeline (Collection.search_records/2Records.Search.search_records/3build_search/3) already supports passing a query string and treats {:query, ""} as a no-op.

Alternatives considered and rejected:

  • New dedicated endpoint (GET /api/v1/collection/search): Adds a route and controller action for no benefit. The existing endpoint already returns searchable data — adding a query parameter is simpler and REST-idiomatic.
  • Separate search action in the controller: Would duplicate the pagination and render logic already in index/2. The only difference is the q value — a parameter is cleaner.
  • Validation/sanitization layer: The Records.Search module already handles FTS5 escaping via fts_query_escape/1 (splitting on whitespace, escaping quotes, wrapping in double-quote syntax with * suffix). No additional sanitization is needed.
  • New LiveView action/template: Not applicable. This is API-only, consumed by the Presto client.

3. Completeness and sequencing

All steps are independent and can be done in a single pass:

  1. Extract q param in CollectionController.index/2 — Add query = normalize_query(params["q"]) before the existing limit/offset extraction, and pass query to search_records_count/1 and search_records/2 instead of "". Add a private normalize_query/1 helper that returns "" for nil or empty string input.

  2. Add tests to collection_controller_test.exs — Six test cases in the describe "GET /api/v1/collection" block:

    • Existing test unchanged (no q param → returns all records)
    • q with matching text → returns filtered results
    • Empty q string → returns all records (same as absent)
    • q with no matches → returns total: 0, empty records
    • q with pagination (limit/offset) → respects both search and pagination
    • Response shape validation → confirms the schema is unchanged
  3. Run the full test suitemix test test/music_library_web/controllers/collection_controller_test.exs

4. Verifiability

Step Verification
Controller change mix test test/music_library_web/controllers/collection_controller_test.exs — all tests pass
No regression mix test full suite — all tests pass
Manual API check curl -H "Authorization: Bearer <token>" "localhost:4002/api/v1/collection?q=beatles" returns filtered results
Empty query curl ... "localhost:4002/api/v1/collection?q=" returns all records
Pagination + search curl ... "localhost:4002/api/v1/collection?q=beatles&limit=1&offset=0" returns correct count with single result

5. Architecture impact analysis

Touchpoint Impact
MusicLibraryWeb.CollectionController (controller) Changedindex/2 reads q param and a new normalize_query/1 helper is added
MusicLibrary.Collection (context) Unchanged — already accepts query string
MusicLibrary.Records.Search (search module) Unchanged — already handles empty queries
MusicLibraryWeb.CollectionJSON (JSON view) Unchanged — response format is identical
Router (router.ex) Unchanged — query params don't affect routing
Schemas (SearchIndex, Record) Unchanged — no schema changes
PubSub Unchanged — no broadcast events involved
Supervision tree Unchanged — no new processes
External APIs Unchanged — no external calls
Presto client Unchanged — the client already sends requests; the q param is opt-in

6. Performance profile

  • Query complexity: Unchanged from the existing FTS5 path. The query already runs records_search_index MATCH <escaped_query> with the same LIMIT/OFFSET pattern. An empty q produces the same query plan as today (no FTS clause → full scan with LIMIT/OFFSET on purchased_at IS NOT NULL).
  • N+1 risk: None. SearchIndex is a pre-joined materialization that includes all needed fields (artists, genres as JSON). The essential_fields() macro selects all columns in a single query.
  • Memory: Unchanged. Pagination caps results at 20 records by default, same as today.
  • Latency: FTS5 queries on the records_search_index table are sub-millisecond for typical collection sizes. The index is maintained automatically by SQLite triggers.

7. Benchmarking requirements

No benchmarks needed. This change does not introduce new query patterns — it reuses the existing FTS5 search path. The performance characteristics are identical whether q is empty or populated (both paths already exist in build_search/3).

8. Cost profile

No cost impact. No external API calls, no additional storage, no compute changes. The FTS5 search runs entirely within the local SQLite database.

9. Production infrastructure steps

No production changes required. The route is unchanged (query parameters don't affect routing), no new environment variables, no migrations, no service provisioning. The change is a standard code deploy.

10. Documentation updates

No documentation updates needed. The API endpoint already exists in router.ex. The q parameter is additive and optional — existing API consumers are unaffected. If project API docs exist externally, note that GET /api/v1/collection now accepts ?q=<search term> as an optional parameter, but this is self-documenting via the REST pattern.

Implementation Notes

Implementation complete. Changes:

  1. Controller (lib/music_library_web/controllers/collection_controller.ex):

    • Added query = normalize_query(params["q"]) in index/2
    • Pass query to search_records_count/1 and search_records/2 instead of hardcoded ""
    • Added private normalize_query/1 helper: nil → "", "" → "", otherwise passthrough
  2. Tests (test/music_library_web/controllers/collection_controller_test.exs):

    • 5 new tests added:
      • q param matches → returns filtered results
      • Empty q string → returns all records
      • q with no matches → returns total: 0, empty records
      • q with pagination → respects limit/offset on filtered results
      • Response shape validation → confirms schema is unchanged
  3. Full test suite: 968 passed (0 failures), no regressions

Final Summary

Summary

Extended GET /api/v1/collection to accept an optional q query parameter for FTS5 full-text search.

Changes

Controller (lib/music_library_web/controllers/collection_controller.ex):

  • index/2 now reads params["q"] and normalizes nil/empty → "" via a new private normalize_query/1 helper
  • Passes the query string to the existing Collection.search_records_count/1 and Collection.search_records/2 pipeline (previously hardcoded "")

Tests (test/music_library_web/controllers/collection_controller_test.exs):

  • 5 new tests: matching query, empty query string, no-match query, pagination with search, and response shape validation

Key points

  • No new search logic — reuses existing Records.Search.build_search/3 which already handled empty queries as a no-op
  • Response format unchanged — CollectionJSON.index/1 returns the same shape
  • No route/schema/migration changes required
  • Full suite: 968 passed, 0 regressions