Search Ranking Service
Build a search service in Ruby that indexes records (for example products or articles), then returns the best matches for a query, ranked by relevance, with filters (such as category and price range) and the matching words highlighted. You'll build the search engine yourself instead of using Elasticsearch: text is split into normalized words, an inverted index in Redis maps each word to the records containing it, and results are ranked with TF-IDF or BM25, the scoring behind most search engines. A Rails API accepts records to index and serves searches, keeping the index up to date as records change. You'll learn how search engines work, text normalization, inverted indexes, relevance scoring, Redis data structures, and measuring search quality.
- Estimate
- ~15.5h
- Steps
- 7
- Completed by
- 0
- Proposed by
- codeseed.app
Rails · RubyGems · Redis
Project roadmap
- 01
Design the search service
~2.5hWrite DESIGN.md: the record format (id, title, body, category, price), text processing (lowercase, remove accents and punctuation, stop words, simple stemming), the Redis layout (a sorted set per word with record ids scored by term frequency, per-record hashes with stored fields and length, and sets for filter values), the scoring formula (BM25 with a title boost), and how updates and deletes keep the index consistent. Done when: DESIGN.md explains indexing, querying, scoring and updates.
- 02
Build the text analyzer
~2hWrite a plain Ruby Analyzer class that turns text into a list of terms: unicode normalization and accent removal, lowercasing, splitting on non-letters, removing stop words, and a simple stemmer (or the ruby-stemmer gem). Use the same analyzer for indexing and for queries. Done when: unit tests show Running shoes! and running shoe produce the same terms.
- 03
Index records in Redis
~2.5hCreate a Rails API with POST /records (index or update) and DELETE /records/:id. Indexing removes the record's old terms first, then adds its new terms, stored fields and filter values, all in a MULTI transaction so searches never see a half-updated record. Keep the document count and average length up to date for scoring. Done when: indexing, updating and deleting records keeps the index consistent.
- 04
Search and rank
~3hAdd GET /search?q=&category=&min_price=&max_price=&page=. Analyze the query, get candidate records from the terms' sorted sets, compute BM25 scores (in Ruby, or with ZUNIONSTORE weights plus a Lua script), give title matches a boost, apply filters by intersecting with the filter sets, and return a page of results with scores. Done when: searches return relevant records in a sensible order, and filters work.
- 05
Highlight matches
~1.5hFor each result, return a snippet of the body around the first matching words with the matches wrapped in <mark> tags, and the title with its matches highlighted, escaping everything else so it's safe to show in HTML. Done when: results show readable snippets with the searched words highlighted.
- 06
Make it robust
~1.5hLimit query length and page size, return empty results (not errors) for queries of only stop words, add a bulk reindex task that rebuilds the index from a source database, and handle Redis being unavailable with a clear 503. Log each search with its duration and result count. Done when: bad queries and Redis outages are handled cleanly, and reindexing works.
- 07
Test and measure quality
~2.5hWrite RSpec tests for the analyzer, indexing consistency, updates removing old terms, scoring order on a tiny known dataset, filters and highlighting. Create 20 test queries with the records you expect at the top, and measure precision at 5, recording the results in README.md before and after tuning the title boost. Done when: the tests pass, and README.md shows the measured search quality.
Ready to build this?
Get a GitHub repo and start building. Your AI reviewer checks each step as you go.
Tech stack